理解delete
在javascript中,我们有时候要使用delete删除对象。但是,对于delete的一些细节我们未必尽知。昨天,看到kangax 分析delete的文章,获益匪浅。本文将文章的精华部分翻译出来,与各位分享。
- 原理
- 代码类型
- 执行上下文
- 激活对象/可变对象
- 属性特性
- 内置对象和DontDelete
- 未声明的赋值
- Firebug 困惑
- 通过eval删除变量
- 浏览器兼容性
- Gecko DontDelete bug
- IE bugs
- 误区
- ‘delete’和宿主对象
- ES5严格模式
- 总结
原理
为什么我们能删除一个对象的属性?
var o = { x: 1 }; delete o.x; // true o.x; // undefined
但是,像这样声明的变量则不行:
var x = 1; delete x; // false x; // 1
或者如此声明的函数:
function x(){} delete x; // false typeof x; // "function"
注意,当一个属性不能被删除时,delete只返回false。
要理解这一点,我们首先需要掌握像变量实例化和属性特性这样的概念--遗憾的是这些在关于javascript的书中很少讲到。我将在接下来的几个 段落中试着简明的重温这些概念。 理解它们一点也不难,如果你不在乎它们为什么这么运行,你可以随意的跳过这一章。
代码类型
在ECMAScript中有三种类型的可执行代码:全局代码(Global code)、函数 代码(Function code)和Eval code。这些类型有那么点自我描述,但这里还是 作一个简短的概述:
- 当一段源代码正文被视为程序时,它在全局作用域中执行,被当成全局代码(Global code)。在一个浏 览器环境中,SCRIPT元素中的内容通常被当作程序来解析,因此,它被当作全局代码来评估。
- 在一个函数内部直接执行的任何代码,很明显被当作函数代码(Function code)。在浏览 器红中事件属性的内容(如:
<p onclick="...">)通常被当作函数代码(Function code)来解析; - 最后,提供给内置函数eval()的文本被当作Eval 代码(Eval code)来解析。我们 很快会看到这种类型很特殊。
执行上下文
当ECMAScript 代码执行时,它总是在一定的上下文中运行,执行上下文是一个有点抽象的实体,它有助于我们理解作用域和变量实例化如何工作的。对于三种类型的可执行代码, 每个都有执行的上下文。当一个函数执行时,可以说控制进入到函数代码(Function code)的执行上下文。全局代码执行时,进入到全局代码(Global code)的执行上下文。
正如你所见,执行上下文逻辑上来自一个栈。首先可能是有自己作用域的全局代码,代码中可能调用一个函数,它有自己的作用域,函数可以调用另外一个函 数,等等。即使函数递归地调用它自身,每一次调用都进入一个新的执行上下文。
激活对象/可变对象
每一个执行上下文在其内部都有一个所谓的可变对象。与执行上下文类似,可变对象是一个抽象的实体,一个描述变量示例化的机制。现在,最有趣的是在源 代码中声明的变量和函数被当作这个可变对象的属性被添加。
当控制进入全局代码的执行上下文时,一个全局对象用作可变对象。这也正是为什么在全局范围中声明的变量或者函数变成了全局对象的属性。
/* remember that `this` refers to global object when in global scope */ 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
Ok,全局变量变成了全局对象的属性,但是,那些在函数代码(Function code)中定义的局部变量又会如何呢?行为其实很相似:它成了可变对象的属性。唯一的差别在于在函数代码(Function code)中,可变对象不是全局对象,而是所谓的激活对象。每次函数代码(Function code)进入执行作用域时,激活对象即被创建。
不仅函数代码(Function code)中的变量和函数成为激活对象的属性,而且函数的每一个参数(与形参相对应的名称)和一个特定Arguments 对象(Arguments )也是。注意,激活对象是一种内部机制,不会被程序代码真正访问到。
(function(foo){ var bar = 2; function baz(){} /* In abstract terms, Special `arguments` object becomes a property of containing function's Activation object: ACTIVATION_OBJECT.arguments; // Arguments object ...as well as argument `foo`: ACTIVATION_OBJECT.foo; // 1 ...as well as variable `bar`: ACTIVATION_OBJECT.bar; // 2 ...as well as function declared locally: typeof ACTIVATION_OBJECT.baz; // "function" */})(1);
最后,在Eval 代码(Eval code)中声明的变量作为正在调用的上下文的可变对象的属性被创建。Eval 代码(Eval code)只使用它正在被调用的哪个执行上下文的可变对象。
var GLOBAL_OBJECT = this; /* `foo` is created as a property of calling context Variable object, which in this case is a Global object */eval('var foo = 1;'); GLOBAL_OBJECT.foo; // 1 (function(){ /* `bar` is created as a property of calling context Variable object, which in this case is an Activation object of containing function */ eval('var bar = 1;'); /* In abstract terms, ACTIVATION_OBJECT.bar; // 1 */})();
属性特性
现在变量会怎样已经很清楚(它们成为属性),剩下唯一的需要理解的概念是属性特性。每个属性都有来自下列一组属性中的零个或多个特性--ReadOnly, DontEnum, DontDelete 和Internal, 你可以认为它们是一个标记,一个属性可有可无的特性。为了今天讨论的目的,我们只关心DontDelete 特性。
当声明的变量和函数成为一个可变对象的属性时--要么是激活对象(Function code),要么是全局对象(Global code),这些创建的属性带有DontDelete 特性。但是,任何明确的(或隐含的)创建的属性不具有DontDelete 特性。这就是我们为什么一些属性能删除,一些不能。
var GLOBAL_OBJECT = this; /* `foo` is a property of a Global object. It is created via variable declaration and so has DontDelete attribute. This is why it can not be deleted. */ var foo = 1; delete foo; // false typeof foo; // "number" /* `bar` is a property of a Global object. It is created via function declaration and so has DontDelete attribute. This is why it can not be deleted either. */ function bar(){} delete bar; // false typeof bar; // "function" /* `baz` is also a property of a Global object. However, it is created via property assignment and so has no DontDelete attribute. This is why it can be deleted. */ GLOBAL_OBJECT.baz = 'blah'; delete GLOBAL_OBJECT.baz; // true typeof GLOBAL_OBJECT.baz; // "undefined"
内置对象和DontDelete
这就是全部:属性中一个独特的特性控制着这个属性是否能被删除。注意,内置对象的一些属性也有特定的DontDelete 特性,因此,它不能被删除。特定的Arguments 变量(或者,正如我们现在了解的,激活对象的属性),任何函数实例的length属 性也拥有DontDelete 特性。
function(){ /* can't delete `arguments`, since it has DontDelete */ delete arguments; // false typeof arguments; // "object" /* can't delete function's `length`; it also has DontDelete */ function f(){} delete f.length; // false typeof f.length; // "number" })();
与函数参数相对应的创建的属性也有DontDelete 特性,因此也不能被删除。
(function(foo, bar){ delete foo; // false foo; // 1 delete bar; // false bar; // 'blah' })(1, 'blah');
未声明的赋值
您可能还记得,未声明的赋值在一个全局对象上创建一个属性。除非它在全局对 象之前的作用域中的某个地方可见。现在我们知道属性分配与变量声明之间的差异,后者设置了DontDelete 特性,而前者没有--应该很清楚未声明的赋值创建了一个可删除的属性。
var GLOBAL_OBJECT = this; /* create global property via variable declaration; property has <STRONG>DontDelete</STRONG> */var foo = 1; /* create global property via undeclared assignment; property has no <STRONG>DontDelete</STRONG> */bar = 2; delete foo; // false typeof foo; // "number" delete bar; // true typeof bar; // "undefined"
请注意,该特性是在属性创建的过程中确定的(例如:none)。后来的赋值不会修改现有属性已经存在的特性,理解这一点 很重要。
/* `foo` is created as a property with DontDelete */function foo(){} /* Later assignments do not modify attributes. DontDelete is still there! */foo = 1; delete foo; // false typeof foo; // "number" /* But assigning to a property that doesn't exist, creates that property with empty attributes (and so without DontDelete) */this.bar = 1; delete bar; // true typeof bar; // "undefined"
那么,在Firebug中会发生什么呢?为什么在控制台中定义的变量可以被删除,难道与我们刚才了解到相反?很好,我先前说过,当涉及到的变量声 明,Eval 代码(Eval code)有一个特殊的行为。在Eval 代码(Eval code)中声明的变量实际上没有创建DontDelete 特性。
eval('var foo = 1;'); foo; // 1 delete foo; // true typeof foo; // "undefined"confusion
同样,在函数代码(Function code)调用也是如此:
(function(){ eval('var foo = 1;'); foo; // 1 delete foo; // true typeof foo; // "undefined" })();
这是Firebug的异常行为的要点,在控制台的所有文本似乎是作为Eval 代码(Eval code)来解析和执行的,而不是作为一个全局对象或函数对象,显然,任何声明的变量没有DontDelete特 性,因此可以很容易地删除,应该意识到正常全局代码和Firebug控制台之间的分歧。
通过eval删除变量
这个有趣的eval属性,连同ECMAScript 其它方面的技巧可以让我们删除不可删除的属性。在同一个执行上下文中,函数声明能覆盖同一名字的变量。
function x(){ } var x; typeof x; // "function"
注意函数如何获得优先权并覆盖同名变量(或者换句话说,可变对象相同的属性)。这是因为函数声明在变量 声明之后实例化,并且可以覆盖它们。函数声明不仅取代了先前的属性值,而且也取代了属性特性。如果我们通过eval声 明函数,该函数也应该替换自身的属性特性。既然在eval内声明的变量没有DontDelete特 性,那么实例化这个新函数应该从本质上消除属性中现有的DontDelete特性,是的这个属性可以删除(当然也就改 变引用新创建函数的值)。
var x = 1; /* Can't delete, `x` has DontDelete */ delete x; // false typeof x; // "number" eval('function x(){}'); /* `x` property now references function, and should have no DontDelete */ typeof x; // "function" delete x; // should be `true` typeof x; // should be "undefined"
遗憾的是,这类欺骗在我尝试中并不总是运行,我可能丢失了一些东西,或者这种行为过于简单不足以引起注意。
浏览器兼容性
从理论上认识事物的工作原理是有用的,但实际影响是至关重要的。当涉及到variable/property creation/deletion时,浏览器遵循标准吗?在大多数是的。
我写了一个简单的测试包检测Global code、Function code 和Eval code代码delete 运算符的兼容性。测试包同时检查 -- delete运算符的返回值,以及应被删除的属性是否被删除的。delete 运算符返回true或false并不重要,重要的是有DontDelete特 性不被删除,反之亦然。
现代浏览器一般都相当兼容,除了这个我早期提到的这个eval特性。下面的浏览器完全通过测试包:Opera 7.54+、Firefox 1.0+、Safari 3.1.2+、Chrome 4+。
Safari 2.x 和3.0.4在删除函数参数时有些问题,这些属性似乎没有创建DontDelete,所 以可以删除它们。Safari 2.x 甚至有更多问题,删除非引用(例如delete 1)抛出错误;函数声明创建了可删除 属性(但奇怪是变量声明不是),在eval中的变量声明成为不可删除的(但函数声明不是)。
与Safari相似,Konqueror (3.5,但不是 4.3)当删除非引用(例如delete 1)抛出错误, 它错误使函数参数可以删除。
Gecko DontDelete bug
Gecko 1.8.x浏览器--Firefox 2.x、 Camino 1.x、Seamonkey 1.x等显示一个有趣的bug:对一个属性明确地赋值可以删除它的DontDelete特性,即使该属性是通过变量或 函数声明来创建的。
function foo(){} delete foo; // false (as expected) typeof foo; // "function" (as expected) /* now assign to a property explicitly */ this.foo = 1; // erroneously clears DontDelete attribute delete foo; // true typeof foo; // "undefined" /* note that this doesn't happen when assigning property implicitly */ function bar(){} bar = 1; delete bar; // false typeof bar; // "number" (although assignment replaced property)
出乎意料的是,IE5.5 – 8全部通过测试包,删除非引用(例如delete 1)抛出 错误(就像在老版的Safari一样)。但事实上有更严重bug存在IE中,这不会立即显现。这些bug都与全局对象相关。
IE bugs
整个章节仅仅为了IE中的bug,想不到吧!
在IE浏览器中(至少是IE6-IE8),下面的表达式抛出错误(在全局代码中执行):
this.x = 1; delete x; // TypeError: Object doesn't support this action
这个也是一样,但异常不同,只是更有趣:
var x = 1; delete this.x; // TypeError: Cannot delete 'this.x'
IE中看起来好像在全局代码中声明变量不能在全局对象中创建属性。通过赋值创建属性(this.x = 1), 然后通过delete删除x将抛出错误。通过声明创建创建属性(var x = 1),然后通过delete this.x删除将抛出另外一个错误。
但这还没完。实际上通过明确的赋值创建的属性在删除时始终引发错误。这不仅是一个错误,而且创建的属性似乎设置了DontDelete特 性,这当然不应该有:
this.x = 1; delete this.x; // TypeError: Object doesn't support this action typeof x; // "number" (still exists, wasn't deleted as it should have been!) delete x; // TypeError: Object doesn't support this action typeof x; // "number" (wasn't deleted again)
与我们思考的相反,未声明的变量(应该在一个全局对象中创建属性)在IE中创建了可删除属性。
x = 1; delete x; // true typeof x; // "undefined"
但是,如果您尝试通过“this”引用在全局代码中删除它(delete this.x ),一个熟悉的错误弹出:
x = 1; delete this.x; // TypeError: Cannot delete 'this.x'
如果我们总结这些行为,从全局代码中delete this.x 似乎是不成功的。当涉及到 的属性是通过显式声明(this.x = 1 )来创建的,delete 将抛出一个错误。当属性是通过未声明的赋值(x = 1 )或声明(var x = 1 )来创建 属性时,delete 将抛出另一个错误。
另一方面,当涉及到的属性是通过显式声明(this.x = 1 )创建时,delete x 抛出错误。如果一个属性是通过声明(var x = 1 )来创建的,删除根本不会发生,并返回正确的false。如果属性 是通过未声明的方式(x = 1)创建,删除操作将按预期进行。
去年九月我正在思考这个问题,Garrett Smith 建议“在IE中全局可变对象作为一个JScript对象,全局对象有宿主执行”。Garrett 引用Eric Lippert’s blog entry ,我们可以通过一些测试验证这些理论。请注意,this和window似乎引用同一对 象(如果我们相信“===”运算符),但可变对象(在一个声明的函数中的对象)不同于这一点。
/* in Global code */function getBase(){ return this; } getBase() === this.getBase(); // false this.getBase() === this.getBase(); // true window.getBase() === this.getBase(); // true 7.window.getBase() === getBase(); // false
误区
理解事物为什么那么工作是一种难以言说的美,我在网上已经看到了与delete运算符误解相关的误区。例如,在关于栈溢 出的回答(评分出其不意的效果高)中,它自信的解释道:“delete is supposed to be no-op when target isn’t an object property ”。现在,我们已经理解了delete 行为的核心,很清楚这个答案是不准确的。delete 不区分变量和属性(事实上,对于删除,这些都是引用),真正的只关心的是DontDelete特性(和属性存在)。
非常有意思的看到这个误解如何相互影响,在同样一个线程中,有人首先提出要直接删除变量(除非它是在eval中声明,否 则不会生效),接着另外一个人提出一种错误的纠正方法--在全局中可能删除变量,但在函数内不行。
在网站上解释Javascript 最好小心,最好总是抓住问题的核心。
‘delete’和宿主对象
delete 的算法大概是这样:
- 如果操作不是一个引用,返回
true; - 如果一个对象没有直接的属性,返回
true;(我们知道,对象可以是激活对象,可以是全局象); - 如果一个属性存在并有DontDelete特性,返回
false; - 否则,删除属性并返回
true;
但是,宿主对象的delete 运算符的行为难以预测。实际上并没有错:除了少数几 个,宿主对象是允许执行任何类型的运算行为的(按规范),如read(内部的[get]方法)、write(内部的[put]方法)或delete(内 部的[delete]方法)。这个定制的[[Delete]]行为使得宿主对象如此混乱。
在IE中我们已经看到一些古怪的行为,如果删除某些对象(明显作为宿主对象来执行)将抛出错误。Firefox的一些版本在尝试删除window.location时 将抛出错误。当涉及到宿主对象时,你不能信任delete返回的任何值。看看在Firefox会有什么发生:
/* "alert" is a direct property of `window` (if we were to believe `hasOwnProperty`) */ window.hasOwnProperty('alert'); // true delete window.alert; // true typeof window.alert; // "function"
删除window.alert 返回true,虽然这个属性什么也没有,它应该导致这个结 果。它保留了一个引用(因此在第一步中不应该返回true),它是窗口对象的直接属性(因此第二步中不能返回true)。唯一 的办法让delete返回true是在第四步之后真正删除属性。但是,属性是永远不会被删除的。
这个故事的寓意在于永远不要相信宿主对象
ES5严格模式
那么,ECMAScript 5th edition 的严格模式可以拿到台面上来了。一些限制正被引入,当delete运 算符是一个变量、函数参数或函数标识符的直接引用时将抛出SyntaxError。另外,如果属性内部有[[Configurable]] == false,将抛出TypeError。
(function(foo){
"use strict"; // enable strict mode within this function
var bar;
function baz(){}
delete foo; // SyntaxError (when deleting argument)
delete bar; // SyntaxError (when deleting variable)
delete baz; // SyntaxError (when deleting variable created with function declaration)
/* `length` of function instances has { [[Configurable]] : false } */
delete (function(){}).length; // TypeError
})(); 另外,删除未声明的变量(换句话说,没有找到的引用)也抛出SyntaxError。
"use strict";
delete i_dont_exist; // SyntaxError 正如你所理解的那样,考虑到删除变量、函数声明和参数会导致如此多得混淆,所有这些限制就有点意义。与不声不响的忽略删除行为相反,严格模式应该采 取更积极的、更具有描述性的措施。
总结
这篇文章是冗长的,我打算去讨论用delete删除数组选项和它的含义。你可以随时参考MDC 的文章了解具体的解释(或阅读规范,自己实验)。
这是Javascript中delete运算符工作的简短概要:
- 变量和函数声明要么是激活对象的属性,要么是全局对象的属性;
- 属性有一些特性,其中之一就是DontDelete,它决定一个属性是否能删除;
- 在Global 和Function code 中的变量和函数声明总是有DontDelete特 性;
- 函数参数也是激活对象的属性,具有DontDelete特性;
- 在Eval代码中的变量和函数声明总是创建没有DontDelete特性的属性;
- 新的属性总是带有空的特性(因此没有DontDelete特性);
- 宿主对象允许对删除作出反应,无论它们是否愿意如此;
如果你想了解更多这里这里描述的东西,请参阅ECMA-262 3rd edition specification。
我希望你喜欢这篇综述,并能学到新东西。任何疑问、建议、更正,一律欢迎。
相关阅读:
原文地址:Understanding delete
转载地址:http://www.denisdeng.com/?p=858

浙公网安备 33010602011771号