深入理解 JavaScript 中的 delete 操作符
原文链接 https://ronghuaxueleng.github.io/2016/08/22/javascript-%E6%B7%B1%E5%85%A5%E7%90%86%E8%A7%A3-JavaScript-%E4%B8%AD%E7%9A%84-delete-%E6%93%8D%E4%BD%9C%E7%AC%A6/
注:以下为加速网络访问所做的原文缓存,经过重新格式化,可能存在格式方面的问题,或偶有遗漏信息,请以原文为准。
delete 操作符用于删除对象的属性。
似乎很多同学(包括我)对 delete 操作符都是似是而非,为什么有的属性可以被删除,有的却不能被删除?为什么能够删除对象的属性却不能删除变量或函数?在 eval 和 严格模式下,delete 操作符又有哪些特性?等等。。
本文将从基本概念到 ECMPScript 内部原理来学习 delete 操作符。
语法
表达式
delete object.property // 点操作符方式,常用方式
delete object['property'] // 字符串属性名方式
参数
- object - 对象名称,或者返回一个对象的表达式
- property - 将要删除的属性
返回值
delete 操作符返回 true 或 false。当被 delete 的对象的属性存在并且拥有 DontDelete (对象属性的一个内部属性,拥有该内部属性表明该属性不能被删除) 时返回 false(在严格模式下将抛出异常),否则返回 true。需要注意的是,对象属性不存在时也返回 true,所以返回值并非完全等同于删除成功与否。
<!--more-->
delete 删除了什么
delete 操作符用来删除对象的属性,这里所说的属性实际上是属性本身,而不是属性指向的对象(对于引用类型来说)。所以,对于引用类型的值,delete 删除了对象属性的本身,不会删除属性指向的对象。
var o = {};
o.x = new Object();
delete o.x; // 上一行new的Object对象依然存在
o.x; // undefined,o的名为x的属性被删除了
在实际的 JavaScrip t中,delete o.x
之后,Object 对象会由于失去了引用而被垃圾回收, 所以 delete o.x
也就“相当于”删除了 o.x
所指向的对象,但这个动作并不是 ECMAScript 标准,也就是说,即使某个实现完全不删除 Object 对象,也不算是违反 ECMAScript 标准。
下面代码说明了 delete 只删除属性本身,而不会删除属性所指的对象:
var o = {};
var a = { x: 10 };
o.a = a;
delete o.a; // o.a属性被删除
o.a; // undefined
a.x; // 10, 因为{ x: 10 } 对象依然被 a 引用,所以不会被回收
虽然是一个小小的 delete 操作符,其行为却异常复杂。
为什么我们能删除一个对象的属性:
var x = { a: 1 };
delete x.a; // true
x.a; // undefined
但却不能删除一个变量:
var x = 1;
delete x; // false;
x; // 1
也不能删除一个函数:
function x() { };
delete x; // false;
typeof x; // "function"
要知道其中的原委,我们需要先了解一些基本概念。
代码类型
在 ECMAScript 中,有 3 种不同类型的可执行代码:
- 全局代码 (Global code) - 当一段源代码被看成程序 (Program) 时,它将会在全局作用域内执行,并且被认为是全局代码。在浏览器环境中,脚本元素的内容通常被解释为程序,因此被作为全局代码来执行。
- 函数代码 (Function code) - 在一段函数中直接执行的代码就被认为是一段函数代码,在浏览器环境下,节点的事件属性(如
<a onclick="..."
)通常都作为函数代码来解析和执行。 - Eval代码 (Eval code) - 被内置函数
eval
执行的代码文本被解释成 Eval 代码
执行上下文 (Execution context)
代码的执行都是在特定的执行上下文中进行的,对于以上三种代码类型都有一个执行上下文与其对应。比如,当一个函数被执行时,程序控制就进入了函数代码执行上下文;当一段全局代码被执行时,程序控制就进入了全局代码执行上下文,等等。
执行上下文在逻辑上是一个栈(stack)。首先可能有一段全局代码,它拥有属于自己的执行上下文;在这段代码中可能调用一个函数,这个函数同样拥有属于自己的执行上下文;这个函数可能调用另一个函数,等等。即使当函数递归调用自己时,在每一步调用中仍然进入了不同的执行上下文。
执行上下文与作用域链和闭包等概念息息相关,欲深入了解执行上下文可以查阅作用域链和闭包的相关资料。
活动对象 (Activation object) / 变量对象 (Variable Object)
每个执行上下文都有一个与之相关联的变量对象 (Variable object),在某个执行上下文中申明的变量和函数将被当着属性 (properties) 附加到这个变量对象上面。
当在全局代码的执行上下文时,全局对象 (Global object) 成为当前执行上下文的变量对象,在浏览器中就是 window 对象。因此,声明的全局函数和变量就成为全局对象的属性。
/* this 指代全局对象 */
var GLOBAL_OBJECT = this;
var foo = 1;
GLOBAL_OBJECT.foo; // 1
foo === GLOBAL_OBJECT.foo; // true
function bar(){}
typeof GLOBAL_OBJECT.bar; // "function"
GLOBAL_OBJECT.bar === bar; // true
局部变量(那些在函数代码中定义的变量)是怎么样的呢?当在函数代码的执行上下文时,活动对象 (Activation object) 成为当前执行上下文中的变量对象,在函数代码中申明的变量和函数成为活动对象的属性,并且函数的参数(形参名为属性名)和 Arguments 对象(arguments 为属性名)也将成为活动对象的属性。
注意,活动对象只是一个内部描述机制,在程序代码中不能直接访问。
(function(foo) {
var bar = 2;
function baz() {};
/*
抽象的过程
'arguments' 对象成为所在函数的活化对象的属性:
ACTIVATION_OBJECT.arguments = arguments;
...参数 'foo' 也是一样:
ACTIVATION_OBJECT.foo; // 1
...变量 'bar' 也是一样:
ACTIVATION_OBJECT.bar; // 2
...函数 'baz' 也是一样:
typeof ACTIVATION_OBJECT.baz; // "function"
*/
})(1);
在 eval 代码中申明的变量会根据 eval 所在的执行上下文来确定变量将成为哪个变量对象的属性。例如:
在全局环境中执行 eval,那么变量将成为全局对象的属性:
var GLOBAL_OBJECT = this;
eval('var foo = 1');
GLOBAL_OBJECT.foo; // 1;
在函数作用域中执行 eval,变量将成为活动对象的属性:
var GLOBAL_OBJECT = this;
(function () {
eval('var bar = 2;');
/*
抽象过程
ACTIVATION_OBJECT.bar; // 1
*/
})();
// 全局环境下不能访问 bar
this.bar // undefined
属性的内部属性
我们马上就接近本文主题了,从上面我们知道声明的变量成了变量对象(全局对象或活动对象)的属性,同时每个属性都可以拥有一个或多个内部属性:ReadOnly
、DontEnum
、DontDelete
和 Internal
。这里我们关注的是 DontDelete
这个内部属性,拥有这个内部属性的变量表明该变量不能使用 delete 操作符删除。
当被声明的变量和函数成为变量对象的属性时,这些属性在创建时就带上了 DontDelete 这个内部属性。然而,任何显式/隐式赋值的属性不生成 DontDelete。这就是为什么我们能够删除一些属性,但有的却不能删除。
上面的分析可能不好理解,直接看下面的代码:
var GLOBAL_OBJECT = this;
/* 'foo' 是全局对象的一个属性,
它通过变量声明而生成,因此拥有内部属性 DontDelete
这就是为什么它不能被删除
*/
var foo = 1;
delete foo; // false
typeof foo; // "number"
/* 'bar' 是全局对象的一个属性,
它通过变量声明而生成,因此拥有 DontDelete
这就是为什么它同样不能被删除
*/
function bar() {};
delete bar; // false
typeof bar; // "function"
/* 'baz' 也是全局对象的一个属性,
然而,它通过属性赋值而生成,因此没有DontDelete
这就是为什么它可以被删除
*/
GLOBAL_OBJECT.baz = "baz";
delete GLOBAL_OBJECT.baz; // true
typeof GLOBAL_OBJECT.baz; // "undefined"
内置对象和DontDelete
所以,某些属性不能被删除的根本原因在于:这些属性拥有内部属性 DontDelete
,该内部属性控制着该属性是否可以被删除。注意:内置对象的一些属性拥有内部属性 DontDelete
,因此不能被删除; 特殊的 arguments 变量(活化对象的属性)拥有 DontDelete
; 任何函数实例的 length (返回形参长度)属性也拥有 DontDelete
:
(function() {
//不能删除'arguments',因为有DontDelete
delete arguments; // false;
typeof arguments; // "object"
//也不能删除函数的'length',因为有DontDelete
function f() {};
delete f.length; // false;
typeof f.length; // "number"
}) ();
与函数 arguments 相关联的属性也拥有 DontDelete,同样不能被删除:
(function(foo, bar) {
delete foo; // false
foo; // 1
delete bar; // false
bar; // "bah"
}) (1, "bah");
未声明的变量赋值
我们知道,直接给未声明的变量赋值会成为全局对象的属性,除非这一属性在作用域链内的其他地方被找到。而我们之前提到过,属性赋值和变量声明的区别:后者生成 DontDelete
而前者不生成,这也就是为什么未声明的变量赋值可以被删除。
var GLOBAL_OBJECT = this;
/* 通过变量声明生成全局对象的属性,拥有 DontDelete */
var foo = 1;
/* 通过未声明的变量赋值生成全局对象的属性,没有 DontDelete */
bar = 2;
delete foo; // false
delete bar; // true
注意:内部属性是在属性生成时确定的,之后的赋值过程不会改变已有的属性的内部属性。理解这点非常重要。
/* 'foo'创建的同时生成 DontDelete */
function foo() {};
/* 之后的赋值过程不改变已有属性的内部属性,DontDelete仍然存在 */
foo = 1;
delete foo; // false;
typeof foo; // "number"
/* 但赋值一个不存在的属性时,创建了一个没有内部属性的属性,因此没有 DontDelete */
this.bar = 1;
delete bar; // true;
typeof bar; // "undefined"
原型中声明的属性和对象自带的属性
原型 prototype 中声明的属性和对象自带的属性(其实这些属性也是在原型 prototype 中的)可以认为是带有 DontDelete 特性的,无法被删除。例如:
//原型中声明的属性无法被删除
function C() { this.x = 42; }
C.prototype.x = 12;
var o = new C();
o.x; // 42, 构造函数中定义的o.x
delete o.x;
o.x; // 12, prototype中定义的o.x,即使再次执行delete o.x也不会被删除
//对象自带的属性无法被删除
var re = /abc/i;
delete re.ignoreCase;
re.ignoreCase; // true, ignoreCase无法删除
Eval 和 Firebug 控制台
在 console 中的所有文本都会被当做 eval 代码来解析和执行,而不是全局或函数代码。我之前说过,eval 在处理变量声明时有一个特殊的行为:在 eval 中声明的变量事实上没有 DontDelete 属性。所以,下面声明的所有变量最后都没有 DontDelete 这个内部属性,所以它们都能被删除。所以要小心普通的全局代码和 Firebug 控制台中代码的区别。
eval('var foo = 1;');
foo; // 1
delete foo; // true
typeof foo; // "undefined"
在函数代码中也是一样:
(function() {
eval('var foo = 1;');
foo; // 1
delete foo; // true
typeof foo; // "undefined"
}) ();
但是这也有一点例外,在 eval 代码中的函数内部通过 var
定义的变量具有 DontDelete,不能被删除。
eval("(function() { var x = 42; delete x; return x; })();");
// 返回 42
浏览器兼容性
了解事物的工作原理是重要的,但实际的实现情况更重要。浏览器在创建和删除变量/属性时都遵守这些标准吗? 对于大部分来说,是的。
这里有一个简单的测试集来检查全局代码、函数代码和 Eval 代码的遵守情况。 测试单元同时检测了 delete 操作的返回值和属性是否像预期那样被删除。delete 的返回值并不像它的实际结果那样重要,delete 操作返回 true 或 false 并不重要, 重要的是拥有/没有 DontDelete 的属性是否被删除。
现代浏览器总的来说还是遵守删除规则的,以下浏览器全部通过测试: Opera 7.54+,Firefox 1.0+,Safari 3.1.2+,Chrome 4+。
Safari 2.x 和 3.0.4 在删除函数 arguments 时存在问题,似乎这些属性在创建时不带 DontDelete,因此可以被删除。Safari 2.x 还有其他问题——删除无引用时(例如 delete 1)抛出错误(译者按:IE 同样有);函数声明生成了可删除的属性(奇怪的是变量声明则正常);eval 中的变量声明变成不可删除(而 eval 中的函数声明则正常)。
与 Safari 类似,Konqueror(3.5,而非4.3)在 delete 无引用和删除 arguments 是也存在同样问题。
Gecko DontDelete bug Gecko 1.8.x 浏览器—— Firefox 2.x, Camino 1.x, Seamonkey 1.x, etc. ——存在一个有趣的 bug:显式赋值值给一个属性能移除它的 DontDelete,即使该属性通过变量或函数声明而生成。
function foo() { };
delete foo; // false;
typeof foo; // "function"
this.foo = 1;
delete foo; // true
typeof foo; // "undefined"
令人惊讶的是,IE5.5-8 也通过了绝大部分测试,除了删除非引用抛出错误(e.g. delete 1,就像旧的 Safari)。 但是,虽然不能马上发现,事实上 IE 存在更严重的 bug,这些 bug 是关于全局对象。
IE bugs
在 IE 中(至少在 IE6-8 中),下面的表达式抛出异常(在全局代码中):
this.x = 1;
delete x; // TypeError: Object doesn't support this action
下面则是另一个:
var x =1;
delete this.x; // TypeError: Cannot delete 'this.x'
// 译者按:在IE8下抛出此异常,在IE6,7下抛出的是和上面一样的异常
这似乎说明,在 IE 中在全局代码中的变量声明并没有生成全局对象的同名属性。 通过赋值创建的属性(this.x = 1)然后通过 delete x 删除时抛出异常; 通过变量声明(var x = 1)创建的属性然后通过 delete this.x 删除时抛出另一个(译者按:在 IE6,7 下错误信息与上面的相同)。
但不只是这样,事实上通过显式赋值创建的属性在删除时总是抛出异常。 这不只是一个错误,而是创建的属性看上去拥有了 DontDelete 内部属性,而按规则应该是没有的:
this.x = 1;
delete this.x; // TypeError: Object doesn't support this action
delete x; // TypeError: Object doesn't support this action
另一方面,未声明的变量赋值(那些同样生成全局对象的属性)又确实在IE下能够正常删除:
x = 1;
delete x; // true
但如果你试图通过 this 关键字来进行删除(delete this.x),那么上面的异常又将抛出:
x = 1;
delete this.x; //TypeError: Cannot delete 'this.x'
如果归纳一下,我们将发现在全局代码中 delete this.x
永远不会成功。当通过显式赋值来生成属性(this.x = 1)时抛出一个异常; 当通过声明/非声明变量的方式(var x = 1 or x = 1)生成属性时抛出另一个异常。而另一方面,delete x 只有在显示赋值生成属性(this.x = 1)时才抛出异常。
宿主对象(Host Object)
小小总结一下 delete 操作符:
- 如果操作数不是引用类型,则返回
true
- 如果对象没有同名的直接属性,返回
true
(对象可以是活动对象或全局对象) - 如果属性存在但是有 DontDelete 特性, 返回
false
- 其它情况,删除属性并且返回
true
然而,对于宿主对象(host object)的 delete 操作的行为却可能是不可预料的。
我们已经看到了在IE中的一些问题:当删除某些对象(那些实现为了宿主对象)属性时抛出异常。 一些版本的 firefox 当试图删除 window.location 时抛出异常(译者按:IE 同样抛出)。 同样,在一些宿主对象中你也不能相信 delete 的返回值, 例如下面发生在 firefox 中的(译者按:chrome 中同样结果;IE 中抛出异常;opera 和 safari 允许删除,并且删除后无法调用,姑且算’正常‘,尽管,从下面的讨论来看似乎却是不正常的,它们事实上删除了不能删除的属性,而前面的浏览器没有):
/* 'alert'是’window‘的一个直接属性(如果我们能够相信'hasOwnProperty') */
window.hasOwnProperty('alert'); // true
delete window.alert; // true
typeof window.alert; // "function"
delete window.alert
返回 true
,尽管这个属性没有任何条件可能产生这个结果(按照上面的算法):它解析为一个引用,因此不能在第一步返回 true
;它是 window 对象的直接属性,因此不能在第二步返回 true;唯一能返回 true 的是当算法达到最后一步同时确实删除这个属性,而事实上它并没有被删除。(译者按:不,在 opera 和 safari 中确实被删除了...)。
所以这个故事告诉我们永远不要相信宿主对象。
ES5 严格模式
在 ES5 严格模式下,当删除操作指向一个变量/函数参数/函数声明的直接引用时抛出 SyntaxError。 此外,如果属性拥有内部属性 [[Configurable]] == false,将抛出 TypeError:
(function(foo) {
"use strict"; //在函数中开启严格模式
var bar;
function baz;
delete foo; // SyntaxError,当删除函数参数时
delete bar; // SyntaxError,当删除变量时
delete baz; // SyntaxError,当删除由函数声明创建的变量时
/* function实例的length拥有[[Configurable]] : false */
delete (function() {}).length; // TypeError
}) ();
而且,在严格模式下,删除未声明的变量(换句话说,未解析的引用),同样抛出 SyntaxError;同时,在严格模式下未声明的赋值也将抛出异常 ReferenceError:
"use strict";
delete i_dont_exist; // SyntaxError
i_dont_exist_either = 1; // ReferenceError
看了之前给出的变量、函数声明和参数的例子,相信现在你也理解了,所有这些限制都是有其意义的。严格模式采取了更积极的和描述性的措施,而不只是忽略这些问题。
总结
下面是对于 JavaScript 中 delete 操作是如何工作的简短的总结:
- 变量和函数声明都是活化对象(Activation Object) 或全局对象(Global Object)的属性
- 属性拥有内部属性,其中 DontDelete 这个内部属性负责确定一个属性是否能够被删除
- 在全局或者函数代码中的变量和函数声明总是创建带有 DontDelete 特性的属性
- 函数参数总是活动对象的属性, 并且带有 DontDelete
- 在 Eval 代码中声明的变量和函数总是创建不带 DontDelete 特性 的属性
- 新的未声明的属性在生成时带空的内部属性,因此也不带 DontDelete 特性
- 永远不要相信宿主对象对 delete 操作做出的反应
如果你想要对这里所描述的东西更加熟悉的话,请参阅 ECMA-262 3rd edition specification。