三月 5th, 2010

[译]理解删除

前端开发, by army8735.

http://perfectionkills.com/understanding-delete/

原文作者是在读《Object-Oriented Javascript》的Function章节后有感,写下的一篇博文。

书中有一句话:function其实就是一个普通变量——它可以被复制到另一个变量中而不会被删除。如果这样解释的话,这里有个例子:

//army注:这一源代码片段是在Firebug下的Console控制台运行的,放在页面中需改写
var sum = function(a, b) {return a + b;}
var add = sum;
delete sum;
true
typeof sum;
"undefined"

忽略代码中省略的两个分号,你注意到这个代码片段的问题了吗?按那句话所说,删除sum不应该成功;delete语句返回的值不可能是true;typeof sum的结果不会是”undefined”。可是事实却非如此,所有的这一切都是因为:Javascript中不可能删除变量。至少不能以这种方式删除。

这是为什么呢?

要回答这个问题,我们需要理解delete操作符是怎么运作的:即什么可以删除、什么不可以删除、为什么?

原理

我们为什么可以这样删除对象的属性?

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"

注意当一个属性不能被删除时,它会返回false。

要解释这些,我们首先要理解变量的实例化以及属性的特性——很遗憾,这些很少在Javascript书籍中被提到。下面这节将做个简明的介绍,它很容易理解。如果你不关心一切为何按照它目前的样子进行工作的话,可以跳过此节。

代码类型

在ECMAScript中共有3种执行方式:Global、Function、Eval。这些类型某种程度上正如其名,但还不足:

当一份源代码作为程序运行时,它在全局作用域内执行,就会被认为是Global代码。在浏览器环境中,script标签通常被解析为一段程序,因此它是全局代码。

直接运行在function内的代码显然是Function代码。例如浏览器中的<p onclick=”…”>。

另外,作为参数传入evel函数的源代码字符串会以Eval方式执行。我们很快就会看到其特殊性。

运行上下文

当ECMAScript代码运行时,它是发生在一个运行上下文(execution context)中的。运行上下文是个抽象实体,可以帮助我们理解作用域和变量实例化是如何工作的。这3种代码各自有自己的运行上下文。当一个function执行时,就可以说控制器进入Function运行上下文中;当全局代码执行时,就进入了Global运行上下文。

如你所知,运行上下文可以逻辑递归。首先一段代码肯定有自己的Global运行上下文;如果调用了function,那么function也有自己的运行上下文;如果function又调用了另外一个function……如此递归。甚至function调用了它本身,每次也会产生一个新的运行上下文。

激活变量对象/激活对象

每一个运行上下文都和一个变量对象(Variable Object)相关联。类似运行上下文,变量对象是一个抽象实体,一种描述变量实例化的机制。现在有趣的事情来了,源代码中的变量和函数声明,实际上成为这个变量对象上的一组属性。

当控制器进入Global方式的运行上下文时,一个全局对象被创建用来作为变量对象。这就解释了为何定义的全局方法成为了全局对象的属性。

/* 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内的变量和方法声明会成为激活对象的属性,相同的事情还发生在传入的function参数身上,它在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代码只是使用它被调用的运行环境的变量对象。

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
	*/

})();

属性特性

终于说到这里了。现在我们已经清楚变量声明时发生的事情,剩下就是理解属性特性。每个属性可以有任意个以下特性——只读、不可枚举、不可删除、内部的。你可以把它们理解为属性上的选项——有或者没有。因为今天讨论的目的,这里只说不可删除特性。

当声明变量或者函数时,它们成为变量对象(对于function代码来说是激活对象,对于全局代码来说是全局对象)的属性,这些属性产生的同时具有不可删除特性。然而,任何直接或者内部的属性分配在创建属性时都没有不可删除特性。这就是为什么有的属性可以删除有的不可以。

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"

内建和不可删除

一个属性上的特殊特性控制了它能否被删除。注意一些内建的的属性一开始就有不可删除特性,它们是永远不能被删除的。例如arguments、函数实例的length属性等。

(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');

未声明的分配

未声明的变量会在全局对象上生成属性,除非在作用域链中于全局对象之前找到这个变量声明。现在我们知道了属性分配和变量声明之间的区别——后者具有不可删除特性而前者没有——这就解释了为何未经声明的变量可以被删除。

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的困惑

那么在Firebug中发生了什么?为何在控制台console中的声明变量可以被删除?还记得前面说的吗,当eval代码进行变量声明时有个特殊的行为,这些声明的变量没有不可删除特性,所以它们可以被删除。

通过eval删除变量

由于eval行为的特殊性,再加上ECMAScript允许我们删除没有不可删除特性的属性,在同一运行上下文中函数声明可以覆盖已经声明的变量。

function x(){ }
var x;
typeof x; // "function"

注意函数声明是如何获得优先权并覆盖掉原有变量声明的,这是因为函数声明的实例化晚于变量声明的实例化,并且允许覆盖。不仅如此,它还会覆盖属性特性。假如我们通过eval方式来声明函数,也会这样。

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"

不幸的是,我在尝试这段代码的时候失败了,可能哪里出了点错误。

浏览器的让步

原理已然弄清,实践更为重要。浏览器们都按照标准做了吗?答案是:大部分是。

我对以下现代浏览器做了一个测试:Opera 7.54+、Firefox 1.0+、Safari 3.1.2+、Chrome 4+。
Safari 2.x和3.0.4在删除函数参数时有问题,这些属性看上去没有不可删除特性,因此它们竟被删除了。Safari 2.x的问题还不止这些——删除没有引用的变量(如delete 1,army注:我感觉这应该是常量才对)会抛出异常;函数声明具有可删除特性(变量声明没有);eval中的变量声明有不可删除特性(函数声明没有)。

类似的,Konqueror(3.5而非4.3)在删除没有引用的变量的时候会抛出异常,并且允许删除函数参数。

Gecko的不可删除特性bug

基于Gecko 1.8.x的浏览器——Firefox 2.x、Camino 1.x、Seamonkey 1.x等等——有个很有趣的bug:可以删除一个属性的不可删除特性,尽管这个属性是通过变量声明或者函数声明而产生的。

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到IE8居然全部通过了测试!除了那个删除没有引用的变量外。然而即使如此,IE却在另外的方面有着严重的bug!这些bug很隐晦,和全局变量有关。

IE的bug

在IE中(至少是6到8版本),下面的表达式会抛出异常。

this.x = 1;
delete x; // TypeError: Object doesn't support this action

再来看看另外一个,非常有趣:

var x = 1;
delete this.x; // TypeError: Cannot delete 'this.x'

这个情况看上去好像全局代码上的变量声明没有在全局对象上产生属性,甚至删除操作时会抛出异常。

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)

与之相反的是,未声明变量却具有可删除特性。

x = 1;
delete x; // true
typeof x; // "undefined"

但是你若通过this来引用全局的话却又会异常:

x = 1;
delete this.x; // TypeError: Cannot delete 'this.x'

如果概括一下的话,那就是在全局代码中delete this.x从未成功过。

我在2009年9月发现了的这一问题,Garrett Smith回答我说“IE中全局变量对象被实现为一个JScript对象,全局对象却被实现为一个宿主对象(host object)”。(army注:延伸阅读hax的文章:http://hax.javaeye.com/blog/349569

我们可以通过一些测试在某种程度上验证这一回答。注意this和window看上去引用了同一对象(如果===全等操作符可以信赖的话),但是变量对象(函数返回的值)却和this不同。

/* 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

删除和宿主对象

关于delete操作的规则如下:

  • 如果操作数中没有引用值,返回true
  • 如果对象没有直接属性,返回true
  • 如果属性存在并且具有不可删除特性,返回false
  • 其它情况,移除属性并且返回true

然而,IE环境的delete操作在删除宿主对象时却经常让人捉摸不透。

/* "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"

最好的习惯是从不信任宿主对象。

ES5严格模式

ECMAScript 5的严格模式带来了哪些条规?它引进了诸多限制。比如删除引用变量、函数参数和标志符的时候会抛出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

这和在严格模式中使用未声明的变量类似:

"use strict";
i_dont_exist = 1; // ReferenceError

Back Top

回复自“[译]理解删除”

  1. dZSk2w Excellent article, I will take note. Many thanks for the story!

  1. 没有任何引用。

发表回复

Back Top