delete 操作符用于删除对象的属性。
似乎很多同学(包括我)对 delete 操作符都是似是而非,为什么有的属性可以被删除,有的却不能被删除?为什么能够删除对象的属性却不能删除变量或函数?在 eval 和 严格模式下,delete 操作符又有哪些特性?等等。。
本文将从基本概念到 ECMPScript 内部原理来学习 delete 操作符。
语法
表达式
delete object.property // 点操作符方式,常用方式 delete object['property'] // 字符串属性名方式
参数
- object:对象名称,或者返回一个对象的表达式
- property:将要删除的属性
返回值
delete 操作符返回 true 或 false。当被 delete 的对象的属性存在并且拥有 DontDelete (对象属性的一个内部属性,拥有该内部属性表明该属性不能被删除) 时返回 false(在严格模式下将抛出异常),否则返回 true。需要注意的是,对象属性不存在时也返回 true,所以返回值并非完全等同于删除成功与否。
delete 删除了什么
delete 操作符用来删除对象的属性,这里所说的属性实际上是属性本身,而不是属性指向的对象(对于引用类型来说)。所以,对于引用类型的值,delete 删除了对象属性的本身,不会删除属性指向的对象。
var o = {}; o.x = new Object(); delete o.x; // 上一行new的Object对象依然存在 o.x; // undefined,o的名为x的属性被删除了
在实际的 JavaScript中,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。