var x = 1; delete x; // false x; // 1
几个周前,我有幸浏览了一遍Stoyan Stefanov的Object-Oriented Javascript(面向对象的JavaScript?)一书.这本书在亚马逊网站上有着异常高的评价,所以我很好奇地想看一下它到底是否真的值得如此推崇.我从函数那一章开始阅读,作者的叙述方式让我感到愉悦;后面跟的例子也被组织的非常恰当,以渐进的形式展现在读者面前,看起来即使是初学者也能很容易地掌握它.然而,几乎在同时我立刻被一个通篇出现的有趣的关于删除函数的失误迷惑住了.这里同时存在一些其他的错误,例如声明函数和函数表达式之间的不同,但是我们在这里暂时不讨论这些问题. 书中声称"函数被创建成一个普通的变量–它可以被拷贝到一个不同的变量中,也可以被删除".在这些阐述的后面,跟了这样一个例子:
>>> var sum = function(a, b) {return a + b;}
>>> var add = sum;
>>> delete sum
true
>>> typeof sum;
"undefined"
排除掉漏掉两个分号,你还能看出这个例子有什么错误么?按常理来讲,问题在于删除一个sum变量时不会成功的;delete函数返回的不应该是true,sum的类型也不应该是undefined.所有的一切都是因为在JavaScript中变量时不可能被删除的.至少不能用这种方式来删除. 那么这个例子中发生了什么呢?是排版错误?还是故意消遣大家?也许都不是.这整个程序片段都是来自firebug的真实输出,Stoyan一定是用firebug来做过快速的测试.几乎可以肯定firebug在删除行为上有着一些不同寻常的规则.firebug诱导Stoyan犯了错误!那么到底在这期间都发生了什么呢? 为了回答这个问题,我们需要理解delete操作在JavaScript到底是如何运作的:到底什么东西可以被删除,什么不能被删除,为什么会这样.今天我将在这篇文章里详细阐述这些问题.我们将会看到firebug的那怪异的然而事实上并不是非常怪异的行为.我们将深入研究党我们创建一个变量,一个函数,创建属性,然后删除它们的时候表象的背后都发生了些什么.我们将看到浏览器的普遍行为和一些臭名昭著的bugs;我们也会讨论到ECMAScript第五版的严格模式是如何改变delete的行为的. 如你所想,网络上关于delete的文献是极其稀少的.MDC(指Firefox文档库,可以去google搜索)里的文章可能是最全面的资源,但是碰巧在这个主题上缺少了一些有趣的细节研究;奇怪的是,正式这些被忽略的细节中的一个原因引起了firebug的奇怪的行为.MSDN参考文档则基本没有参考价值.
Theory(原理)
那么为什么我们可以删除对象的属性呢:
var o = { x: 1 };
delete o.x; // true
o.x; // undefined
而不是变量呢?就像这样:
var x = 1; delete x; // false x; // 1
或者是function,例如这样:
function x(){}
delete x; // false
typeof x; // "function"
注意:当一个属性不能被删除的时候delete方法将返回一个false值.
要理解为什么会出现这样的情况,我必须首先领会变量的实例化和property属性,它们通常很不幸地被一些有关JavaScript的书籍所遗漏.我将试着在接下来的几段内非常简明地概括这些特性.事实上理解它们并不困难!如果你并不关心事物内在的运行机制,那么你可以自由选择跳过这几章.
Type of code
在ECMAScript中有三种可以执行的代码:全局代码,Function 代码和eval代码.这些类型都有着显而易见的含义,但是下面还是列出一些简短的概要: 1.当一个源文件被解释成程序后,它将在全局变量里被执行,并且将被解释成全局代码.在一个浏览器环境中,script标签的内容通常被解析成一个程序,因此被认为是全局代码. 2.任何直接在函数里执行的代码,很明显被认为是一段Function代码.在浏览器中,dom元素的事件属性中的内容(例如:<p onclick="...">)都被解析成Function代码. 3.最后,被传给内建的eval函数的文本被解析成Eval代码,我们一会就会知道为什么eval代码的表现是如此特殊.
Execution context(执行上下文)
当ECMAScript代码执行的时候,它通常发生在某个执行上下文中.执行上下文是一个稍微有点抽象的东西,它可以解释作用域和变量实例化是如何运作的.三种代码中的每一种都有一个执行上下文.当一个function被执行的时候,也就是说解释器执行到了function代码的执行上下文;当全局代码被执行的时候,解释器进入到全局代码的执行上下文,等等. 如你所见,执行上下文可以构建一个逻辑上的堆栈.开始的时候可能是全局的代码和它所在的执行上下文;这些代码可能会调用一个函数和函数的执行上下文;接着函数可以调用另一个函数,如此一直调用下去.甚至当函数调用它本身的时候,每次调用也会创建一个新的执行上下文
Activation object / Variable object.
每个执行上下文都有一个叫做Variable Object的东西跟它联系在一起.跟执行上下文类似,Variable object是一个抽象的概念,一种用来表述变量实例的结构.现在,让人感兴趣的部分是源文件中的变量和函数最终都被附加在Variable object的属性上. 当程序进入到全局代码的上下文的时候,一个全局对象被当做Variable object来使用,这也正说明为什么定义在全局范围内的变量和函数都会变成全局对象的属性.
/* 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,全局的变量的确变成了全局对象的属性,那么那些在函数代码里声明的局部变量呢?其实他们的行为非常相似:它们也会变成Variable object的属性.唯一不同之处就是,当在函数代码中的时候,Variable object不是一全局对象,而是一个动态的Activation object对象.每次进入函数代码的执行上下文的时候,都会产生一个Activation object.而且,不仅变量和函数变成了Activation object的属性,函数的参数和一个特殊的对象Arguments也会变成Activation object的属性.注意Activation object是一种内在的运行机制,它的行为永远不会被程序代码所改变.
(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代码里的变量会变成当前调用上下文的Variable object的属性.Eval代码使用它被调用的上下文作为自己执行的上下文.
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
*/
})();
Property attributes
We are almost there. Now that it’s clear what happens with variables (they become properties), the only remaining concept to understand is property attributes.
Every property can have zero or more attributes from the following set — ReadOnly, DontEnum, DontDelete andInternal. You can think of them as flags — an attribute can either exist on a property or not. For the purposes of today’s discussion, we are only interested in DontDelete. When declared variables and functions become properties of a Variable object — either Activation object (for Function code), or Global object (for Global code), these properties are created with DontDelete attribute. However, any explicit (or implicit) property assignment creates property without DontDelete attribute. And this is essentialy why we can delete some properties, but not others:
我们越来越接近真理了.现在我们已经清楚了变量都发生了什么(它们变成了属性), 剩下的我们就是要理解所谓的"属性的特性"了(property attributes这里我译成这样,可能表达有问题,但是想不出更好的翻译方法).每个属性都有0个或者多个如下的特性–ReadOnly, DontEnum, DontDelete and Internal. 你可以将他们理解成一些标记-特性是可有可无的.对于今天的讨论来说,我们关心的只是DontDelete特性.当一个变量或者函数变成某个Variable object–不管是Activation object还是全局对象的属性的时候,他们的特性都会被标记为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"
Built-ins and DontDelete
这就是所有的全部:一个特殊的属性特性控制着某个属性是否能被删除.注意某些内建对象的某些属性也拥有 DontDelete特性,所以不能被删除.arguments变量同样拥有DontDelete特性,任何函数实例的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"
})();
通过参数创建的属性也不能被删除:
(function(foo, bar){
delete foo; // false
foo; // 1
delete bar; // false
bar; // 'blah'
})(1, 'blah');
Undeclared assignments
正如你所记得的,未声明的变量会在全局对象上生成属性.除非在找到全局对象之前的作用域链上找到了这个属性.现在我们知道了属性赋值和变量声明的区别,后者带着不可删除的特性,前者却是可以删除的–也应该很容易理解为什么没有声明的变量可以被轻易删除.
var GLOBAL_OBJECT = this; /* create global property via variable declaration; property has DontDelete */ var foo = 1; /* create global property via undeclared assignment; property has no DontDelete */ bar = 2; delete foo; // false typeof foo; // "number" delete bar; // true typeof bar; // "undefined"
注意,在属性被创建的时候,它的特性就被确定了(除了ie).后来的赋值和分配都不会印象已经存在的属性的特性.理解这种差别是很重要的.
/* `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 confusion
那么在firebug里为什么发生了和理论相悖的事情呢?为什么在命令行里声明的变量可以被删除?正如我说过的.Eval代码当声明变量的时候有着特殊的行为 ,在eval里声明的变量会变成一个没有DontDelete特性的属性.
eval('var foo = 1;');
foo; // 1
delete foo; // true
typeof foo; // "undefined"
and, similarly, when called within Function code:
(function(){
eval('var foo = 1;');
foo; // 1
delete foo; // true
typeof foo; // "undefined"
})();
这就是为什么firebug表现出如此不同寻常的行为.所有在 控制台中输入的代码都会被当做eval代码处理.很明显在此声明的变量都会成为没有DontDelete特性的属性,所以随后可以被删除.
Deleting variables via eval
利用eval这种行为和ECMAscript的另一个特殊行为,我们可以巧妙地删除有着不可删除特性的属性,这个行为就是声明函数的时候如果在上下文中已经存在同名的变量,将会覆盖之.
function x(){ }
var x;
typeof x; // "function"
Note how function declaration takes precedence and overwrites same-named variable (or, in other words, same property of Variable object). This is because function declarations are instantiated after variable declarations, and are allowed to overwrite them. Not only does function declaration replaces previous value of a property, it also replaces that property attributes. If we declare function via eval, that function should also replace that property’s attributes with its own. And since variables declared from within eval create properties without DontDelete, instantiating this new function should essentially remove existing DontDelete attribute from the property in question, making that property deletable (and of course changing its value to reference newly created function).
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"
Unfortunately, this kind of spoofing doesn’t work in any implementation I tried. I might be missing something here, or this behavior might simply be too obscure for implementors to pay attention to.
Browsers compliance
Knowing how things work in theory is useful, but practical implications are paramount. Do browsers follow standards when it comes to variable/property creation/deletion? For the most part, yes. I wrote a simple test suite to check compliance of delete operator with Global code, Function code and Eval code. Test suite checks both — return value of delete operator, and whether properties are deleted (or not) as they are supposed to. delete return value is not as important as its actual results. It’s not very crucial if delete returns true instead of false, but it’s important that properties with DontDelete are not deleted and vice versa. Modern browsers are generally pretty compliant. Besides this eval peculiarity I mentioned earlier, the following browsers pass test suite fully: Opera 7.54+, Firefox 1.0+, Safari 3.1.2+, Chrome 4+. Safari 2.x and 3.0.4 have problems with deleting function arguments; those properties seem to be created without DontDelete, so it is possible to delete them. Safari 2.x has even more problems — deleting non-reference (e.g. delete 1) throws error; function declarations create deletable properties (but, strangely, not variable declarations); variable declarations in eval become non-deletable (but not function declarations). Similar to Safari, Konqueror (3.5, but not 4.3) throws error when deleting non-reference (e.g. delete 1) and erroneously makes function arguments deletable.
Gecko DontDelete bug
Gecko 1.8.x browsers — Firefox 2.x, Camino 1.x, Seamonkey 1.x, etc. — exhibit an interesting bug where explicitly assigning to a property can remove its DontDelete attribite, even if that property was created via variable or function declaration:
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)
Surprisingly, Internet Explorer 5.5 – 8 passes test suite fully except that deleting non-reference (e.g. delete 1) throws error (just like in older Safari). But there are actually more serious bugs in IE, that are not immediately apparent. These bugs are related to Global object.
IE bugs
The entire chapter just for bugs in Internet Explorer? How unexpected! In IE (at least, 6-8), the following expression throws error (when evaluated in Global code):
this.x = 1;
delete x; // TypeError: Object doesn't support this action
and this one as well, but different exception, just to make things interesting:
var x = 1;
delete this.x; // TypeError: Cannot delete 'this.x'
It’s as if variable declarations in Global code do not create properties on Global object in IE. Creating property via assignment (this.x = 1) and then deleting it via delete x throws error. Creating property via declaration (var x = 1) and then deleting it via delete this.x throws another error. But that’s not all. Creating property via explicit assignment actually always throws error on deletion. Not only is there an error, but created property appears to have DontDelete set on it, which of course it shouldn’t have:
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)
Now, contrary to what one would think, undeclared assignments (those that should create a property on global object) do create deletable properties in IE:
x = 1;
delete x; // true
typeof x; // "undefined"
But if you try to delete such property by referecing it via this in Global code (delete this.x), a familiar error pops up:
x = 1;
delete this.x; // TypeError: Cannot delete 'this.x'
If we were to generalize this behavior, it would appear that delete this.x from within Global code never succeeds. When property in question is created via explicit assignment (this.x = 1), delete throws one error; when property is created via undeclared assignment (x = 1) or via declaration (var x = 1), delete throws another error. delete x, on the other hand, only throws error when property in question is created via explicit assignment — this.x = 1. If a property is created via declaration (var x = 1), deletion simply never occurs and delete correctly returns false. If a property is created via undeclared assignment (x = 1), deletion works as expected. I was pondering about this issue back in September, and Garrett Smith suggested that in IE “The global variable object is implemented as a JScript object, and the global object is implemented by the host. Garrett used Eric Lippert’s blog entry as a reference. We can somewhat confirm this theory by performing few tests. Note how this and window seem to reference same object (if we can believe=== operator), but Variable object (the one on which function is declared) is different from whatever this references.
/* in Global code */
function getBase(){ return this; }
getBase() === this.getBase(); // false
this.getBase() === this.getBase(); // true
window.getBase() === this.getBase(); // true
window.getBase() === getBase(); // false
Misconceptions
The beauty of understanding why things work the way they work can not be underestimated. I’ve seen few misconceptions on the web related to misunderstanding of delete operator. For example, there’s this answer on Stackoverflow (with surprisingly high rating), confidently explaining how “delete is supposed to be no-op when target isn’t an object property”. Now that we understand the core of delete behavior, it becomes pretty clear that this answer is rather inaccurate. delete doesn’t differentiate between variables and properties (in fact, for delete, those are all References) and really only cares about DontDelete attribute (and property existence). It’s also interesting to see how misconceptions bounce off of each other, where in the very same thread someone first suggests to just delete variable (which won’t work unless it’s declared from within eval), and another person provides a wrong correction how it’s possible to delete variables in Global code but not in Function one. Be careful with Javascript explanations on the web, and ideally, always seek to understand the core of the issue
`delete` and host objects
An algorithm for delete is specified roughtly like this:
- If operand is not a reference, return
true - If object has no direct property with such name, return
true(where, as we now know, object can be Activation object or Global object) - If property exists but has DontDelete, return
false - Otherwise, remove property and return
true
However, behavior of delete operator with host objects can be rather unpredictable. And there’s actually nothing wrong with that: host objects are allowed (by specification) to implement any kind of behavior for operations such as read (internal [[Get]] method), write (internal [[Put]] method) or delete (internal [[Delete]] method), among few others. This allowance for custom [[Delete]] behavior is what makes host objects so chaotic. We’ve already seen some IE oddities, where deleting certain objects (which are apparently implemented as host objects) throws errors. Some versions of Firefox throw when trying to delete window.location. You can’t trust return values of delete either, when it comes to host objects; take a look at what happens in 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"
Deleting window.alert returns true, even though there’s nothing about this property that should lead to such result. It resolves to a reference (so can’t return true on the first step). It’s a direct property of a window object (so can’t return true on a second step). The only way deletecould return true is after reaching step 4 and actually deleting a property. Yet, property is never deleted. The moral of the story is to never trust host objects.
ES5 strict mode
So what does strict mode of ECMAScript 5th edition bring to the table? Few restrictions are being introduced. SyntaxError is now thrown when expression in delete operator is a direct reference to a variable, function argument or function identifier. In addition, if property has internal [[Configurable]] == false, a TypeError is thrown:
(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
})();
In addition, deleting undeclared variable (or in other words, unresolved Referece) throws SyntaxError as well:
"use strict";
delete i_dont_exist; // SyntaxError
This is somewhat similar to the way undeclared assignment in strict mode behaves (except that ReferenceError is thrown instead of a SyntaxError):
"use strict";
i_dont_exist = 1; // ReferenceError
As you now understand, all these restrictions somewhat make sense, given how much confusion deleting variables, function declarations and arguments causes. Instead of silently ignoring deletion, strict mode takes more agressive and descriptive measures.
Summary
This post turned out to be quite lengthy, so I’m not going to talk about things like removing array items with delete and what the implications of it are. You can always refer to MDC article for that particular explanation (or read specs and experiment yourself). Here’s a short summary of how deletion works in Javascript:
- Variables and function declarations are properties of either Activation or Global objects.
- Properties have attributes, one of which — DontDelete — is responsible for whether a property can be deleted.
- Variable and function declarations in Global and Function code always create properties with DontDelete.
- Function arguments are also properties of Activation object and are created with DontDelete.
- Variable and function declarations in Eval code always create properties without DontDelete.
- New properties are always created with empty attributes (and so without DontDelete).
- Host objects are allowed to react to deletion however they want.
If you’d like to get more familiar with things described here, please refer to ECMA-262 3rd edition specification. I hope you enjoyed this overview and learned something new. Any questions, suggestions and corrections are as always welcomed.
No tags
