本文出自:http://www.2cto.com/kf/201402/276751.html
!!!!!!!!草稿版本的翻译完成以后,无意中搜索发现了另外一篇翻译,也是同一篇原文,比我翻译的好。 justjavac,迷渡是我很尊敬的一位前辈, 链接如下:javascript 中的 delete
虽然说花了2个星期,进度也不理想,但是至少知道自己还有很多差距,也算是值得的。
教训就是以后做翻译绝对要先搜索一些关键字.
说明: 在本文中,将 property 统一翻译为属性,而将 attribute翻译为特性. 比如eval.length,其中length 就是 eval函数(引用)的一个属性,但是 length这个属性又拥有一些特性,比如 不可删除,不可修改等.几个星期前,我有幸阅读了 斯托诺夫(Stoyan Stefanov) 的 Object-Oriented Javascript 一书.(原文所引的是第一版,现在是第二版,错误应该已经得到修正.)
这本书在亚马逊得到非常高的评价(现在41个评论与5星推荐),所以我就很好奇,看看是不是值得推荐.我从 functions 的章节开始阅读,并非常喜欢书里对事物的解说方式;书中的示例有很合理的结构顺序以及先进的组织方式,即使是初学者也能很容易了解和掌握.然而,几乎很快,我在章节中偶然发现了一个有趣的误解 —— 关于functions 的删除.当然也还有一些其他的错误(比如函数声明和函数表达式的区别),但在本文中我们不谈论其他.
书里说 ” function 被视为正常的变量 - 可以复制到另一个变量,甚至可以被删除.” 在这个解释后面,有一个例子:
1
2
3
4
5
6
|
>>> var sum = function(a, b) {
return
a + b;}
>>> var add = sum;
>>> delete sum
true
>>> typeof sum;
"undefined"
|
那为什么此示例会出错? 这是一个错误?玩笑?应该不是.整个代码片段实际上是 Firebug控制台 的输出, Stoyan 肯定是快速测试过的.原因是Firebug好像采用了一些不同的删除规则. 所以是 Firebug 导致 Stoyan 在这里出错!但这到底是怎么回事呢?
要回答这个问题,我们需要了解在Javascript中 delete操作符的工作机制: 什么可以被删除,什么不能被删除以及为什么.现在我将试图详细解释其原因.我们将发现 Firebug “怪异”的行为并认识到并不是所有都是怪异的,我们将深入研究当声明变量,functions,指定属性和删除它们 时在幕后究竟发生了什么; 我们将列举 浏览器的承诺和一些最臭名昭著的bug;我们也会讨论第五版 ECMAScript的严格模式,以及它如何改变delete操作符的行为.
在本文中 Javascript和ECMAScript 都指的是ECMAScript(除非特别指出是Mozilla的Javascript实现).
不出所料,网络上关于 delete 的解释相当稀少. MDC的文章 可能是最全面的资源,但遗憾的是错过了一些有趣的细节; 奇怪的是,这些被遗忘的事情之一正是Firebug的复杂行为的原因.而 MSDN参考手册 几乎是无用的.
1. 基本原理
我们可以删除对象的某个属性:
1
2
3
|
var o = { x:
1
};
delete o.x;
// true
o.x;
// undefined
|
1
2
3
|
var x =
1
;
delete x;
// false
x;
// 1
|
1
2
3
|
function x(){}
delete x;
// false
typeof x;
// "function"
|
要理解这一点,我们首先需要理解变量实例化和 property 属性等概念——不幸的是在Javascript的书中很少涵盖这些东西.我会在接下来的几个段落简略地介绍这些.这些概念一点都不难理解!如果你不关心 为什么JavaScript工作的方式会如此,请跳过这一章.
1.1 可执行代码的分类
在 ECMAScript 中有3种类型的可执行代码: 全局代码, 函数代码, 以及 Eval 代码.这些类型是自描述的,下面是一个简短的概述:
当一个文本 source 被当做作一段程序,它在全局范围内执行,被认为是全局代码( Global code).在浏览器环境中, SCRIPT 元素的内容通常被解析为程序,因此等价于全局代码.直接在函数内执行的东西,很明显,被认为是一段函数代码( Function code).在浏览器中,事件属性的内容(例如
)通常被解析并被认为是一段函数代码.最后,在内置 eval 函数中的文本被解析为 Eval code. 我们很快就会看到为什么这种类型是特殊的.
1.2 执行上下文
当 ECMAScript 代码执行时,它总是处于特定的执行上下文中的.执行上下文是一个抽象的存在,这有助于理解 scope 和 变量实例 是如何工作的的. 对于三种类型的可执行代码,每种都有一个执行上下文.当一个函数执行时,可以说被控制着进入 Function代码执行上下文;当全局代码执行时,进入全局代码的执行上下文 等等.正如您所见到的,执行上下文在逻辑上形成一个堆栈.首先是全局代码及其执行上下文;而全局代码可以调用一个函数,有函数自己的执行上下文,该函数可以调用另一个函数,等等等等.即使函数递归地调用其本身,每一次调用也会进入一个新的执行上下文.
1.3 Activation对象/Variable对象
每个执行上下文都有一个被叫做 Variable object (活化对象?)的对象与其相关联.类似于执行上下文,Variable 对象也是一个抽象的存在,用来描述变量实例化的一种机制.现在,有意思的是,在一个源文本中声明的变量和函数中实际上都被 添加为该 Variable object 对象的属性(properties).当进入全局代码执行上下文,全局对象(Global object,如浏览器中的 window)被当做其 Variable object 对象.这正是为什么在全局范围内声明的变量或函数会 成为全局对象的属性的原因:
1
2
3
4
5
6
7
8
9
|
/* 当在全局范围时, `this` 指向的就是 global object */
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 )也会成为 Activation object 对象的属性.注意, Activation 对象只是一种内部的机制,程序永远不可能真正地访问(引用)到这个对象.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
(function(foo){
var bar =
2
;
function baz(){}
/*
以抽象虚拟的说法,
专有的 `arguments` 对象成为对应函数 Activation 对象的一个属性(property):
ACTIVATION_OBJECT.arguments; // Arguments 对象
...当然,参数 `foo`也是一样的道理:
ACTIVATION_OBJECT.foo; // 1
...同时,局部变量 `bar`同样如此:
ACTIVATION_OBJECT.bar; // 2
...定义的局部函数也是如此:
typeof ACTIVATION_OBJECT.baz; // "function"
*/
})(
1
);
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
var GLOBAL_OBJECT =
this
;
/* 此处eval内的`foo` 被创建为 调用上下文 Variable 对象的属性,
在此处上下文对象即为全局对象 Global object */
eval(
'var foo = 1;'
);
GLOBAL_OBJECT.foo;
// 1
(function(){
/* `bar` 被创建为 调用上下文 Variable object 对象的一个属性,
此处 Variable object 对象是包含的function的一个 Activation 对象 */
eval(
'var bar = 1;'
);
/*
以一种虚拟抽象的角度看,
ACTIVATION_OBJECT.bar; // 1
*/
})();
|
1.4 Property 属性
该说的基本上都说完了.现在对于变量是如何处理的已经很清楚(他们变成了 properties 属性),剩下唯一需要理解的概念是 property 属性.每个 property 都可以拥有下面列举的0到多个特性 —— ReadOnly, DontEnum, DontDelete以及 Internal.你可以把它们当做 标志位 —— 每一个 property 中可以存在也可以不存在的 attribute .为了下面的讨论,我们只对 DontDelete 感兴趣.当声明的变量和函数成为 Variable object 对象的properties —— 可能是 Activation object 对象(对于 Function code 来说),或全局对象(对全局代码来说),这些 properties 创建时就 被赋予了 DontDelete attribute属性.然而,任何显式(或隐式)对指定 property 属性的赋值所创建的 property 属性则 没有DontDelete属性.这就是为什么我们可以删除某些 properties属性,而其他的不能删除的本质原因:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
var GLOBAL_OBJECT =
this
;
/* `foo` 是 Global object 的一个 property.
是通过变量声明创建的,因此拥有 DontDelete 标志.
这就是为什么不能被删除的原因. */
var foo =
1
;
delete foo;
// false
typeof foo;
// "number"
/* `bar` 是 Global object 的一个 property.
是通过 function 声明创建的,因此拥有 DontDelete 标志.
这也是为什么不能被删除的原因. */
function bar(){}
delete bar;
// false
typeof bar;
// "function"
/* `baz` 同样是 Global object 的一个 property.
但是通过 property 赋值创建的,However,因此没有 DontDelete 标志.
这是为什么可以被删除的原因. */
GLOBAL_OBJECT.baz =
'blah'
;
delete GLOBAL_OBJECT.baz;
// true
typeof GLOBAL_OBJECT.baz;
// "undefined"
|
1.5 内置的和不可删除的
DontDelete指的是: 在 property上的一个特殊attribute,用于控制是否可以删除此property.注意,一些内置对象的属性被指定为 DontDelete,所以不能被删除.特殊的arguments变量(或者,正如我们刚刚介绍的, Activation 对象的属性 等) 拥有 DontDelete 标志.所有 function实例的length 属性(形参个数)也具有 DontDelete 标志:
1
2
3
4
5
6
7
8
9
10
|
(function(){
/* 不能删除 `arguments`,因为其拥有 DontDelete */
delete arguments;
// false
typeof arguments;
// "object"
/* 不能删除 function 的 `length`; it also has DontDelete */
function f(){}
delete f.length;
// false
typeof f.length;
// "number"
})();
|
1
2
3
4
5
6
|
(function(foo, bar){
delete foo;
// false
foo;
// 1
delete bar;
// false
bar;
// 'blah'
})(
1
,
'blah'
);
|
1.6 未声明赋值
您也许还记得,未声明的赋值(没有 var 定义) 将在全局对象上创建一个属性.除非是在查找到全局范围对象之前就已经在作用域链上发现了一个同名的属性.现在我们知道了 property 赋值和变量声明 的区别 —— 后者被设置了 DontDelete标志,而前一个没有被设置 —— 为什么 未声明的赋值 创建的property是可删除的现在就很明显了(没设置 DontDelete标志):
1
2
3
4
5
6
7
8
9
10
11
12
13
|
var GLOBAL_OBJECT =
this
;
/* 通过变量声明 而创建的全局 property; 拥有 DontDelete 标志*/
var foo =
1
;
/* 通过未声明赋值创建的 global property; 没有 DontDelete 标志*/
bar =
2
;
delete foo;
// false
typeof foo;
// "number"
delete bar;
// true
typeof bar;
// "undefined"
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
/* `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 (因此没有 DontDelete 标志) */
this
.bar =
1
;
delete bar;
// true
typeof bar;
// "undefined"
|
2. Firebug 的困惑
那 Firebug 中究竟是怎么回事?为什么在console中声明的变量可以被删除,和我们刚刚学到的相反呢?嗯,正如我之前所说,Eval代码在变成变量声明时有一个特殊的行为. 在Eval代码内声明的变量实际上是没有 DontDelete 标志的:
1
2
3
4
|
eval(
'var foo = 1;'
);
foo;
// 1
delete foo;
// true
typeof foo;
// "undefined"
|
1
2
3
4
5
6
7
|
(function(){
eval(
'var foo = 1;'
);
foo;
// 1
delete foo;
// true
typeof foo;
// "undefined"
})();
|
这就是Firebug的反常行为的要点.似乎控制台中所有的文本都 被当做 Eval代码 解析和执行,而不是全局代码或函数代码.显然,任何声明的变量最终都 没有 DontDelete 标志,因此可以很容易地被删除. 请留意在 常规的全局代码和Firebug控制台中的这些差异.
2.1 通过eval删除变量
这是eval 一个有趣的行为,加上ECMAScript的另一个方面可以技术上允许我们删除non-deletable的 properties.在相同的执行上下文中,关于函数的声明会覆盖同名的变量(原理是在context中,变量声明被提前到整个context最前面,function声明也被提前,但应该是function声明被 提到变量声明之后,所以...):
1
2
3
4
5
6
7
|
function x(){ }
var x;
// 只声明
typeof x;
// "function"
function a(){ }
var a =
6
;
// 声明 + 赋值
typeof a;
// "number"
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
var x =
1
;
/* 不能删除, `x` has DontDelete */
delete x;
// false
typeof x;
// "number"
eval(
'function x(){}'
);
/* `x` property 现在指向 function, 并且没有 DontDelete 标志*/
typeof x;
// "function"
delete x;
// 按理说应该是 `true`
typeof x;
// 按理说应该是 "undefined"
|
3. 浏览器兼容性
理解事物运行的原理是很有用的,但认识实际运行环境更重要.浏览器在处理 变量/属性 的 创建/删除 时遵循标准吗?在大多数情况下,是这样的.我编写了 一个简单的测试脚本 来测试 delete 操作符对全局代码,Function 代码和Eval代码的操作 是否遵循规范.测试脚本检查- delete操作符的返回值,以及当属性应该被删除时是否被删除. delete 的返回值不如其实际结果重要.它不是很重要如果删除返回true,而不是false,但拥有DontDelete特性的properties未被删除倒是很重要的,反之亦然.
现代浏览器的兼容性通常是非常好的.除了我 前面提到过特别的 eval ,以下浏览器对测试套件完全通过:Opera 7.54 +,Firefox 1.0 +,Safari 3.1.2 +,Chrome 4 +.
Safari 2.x 和3.0.4 在删除 function arguments 时有问题; 这些properties 创建时似乎没有赋予DontDelete特性,所以可以被删除. Safari 2.x 甚至有更多的问题 —— 删除非引用(例如delete 1;)将会抛出错误; function 声明会创建 可删除的 properties (但奇怪的是,variable 声明就不能删除); 而在eval中 variable声明又变成non-deletable(但函数声明又不是这样).
类似于 Safari,Konqueror(3.5,而不是4.3)在删除非引用时(例如 delete 1;)会抛出错误,还会错误地允许删除函数 arguments.
3.1 Gecko引擎的DontDelete缺陷
Gecko 1.8.x的浏览器,Firefox 2.x,Camino 1.x,Seamonkey 1.x,等等 —— 表现出一个有趣的bug,当显式地给一个 property 赋值后,会移除其DontDelete 特性,即使该property 是通过变量或函数声明 而创建的:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
function foo(){}
delete foo;
// false (符合预期)
typeof foo;
// "function" (符合预期)
/* 现在显示地给一个 property 赋值*/
this
.foo =
1
;
// 错误地清除了 DontDelete 特性
delete foo;
// true
typeof foo;
// "undefined"
/* 注意当隐式地给 property 赋值时并不会发生 */
function bar(){}
bar =
1
;
delete bar;
// false
typeof bar;
// "number" (赋值操作替换了 property)
|
4. IE的Bugs
整篇文章只有Internet Explorer中有BUG吗?多么的意想不到啊!在IE(至少IE6 - IE8),以下表达式将会抛出错误(在Global code中执行时):
1
2
|
this
.x =
1
;
delete x;
// TypeError: Object doesn't support this action
|
1
2
|
var x =
1
;
delete
this
.x;
// TypeError: Cannot delete 'this.x'
|
但这还不是全部.通过显式赋值创建的属性在删除时 总会抛出错误.不仅此处有一个错误,而且创建的属性似乎还被设置了DontDelete标志,这当然是不应该的:
1
2
3
4
5
6
7
|
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)
|
1
2
3
|
x =
1
;
delete x;
// true
typeof x;
// "undefined"
|
1
2
|
x =
1
;
delete
this
.x;
// TypeError: Cannot delete 'this.x'
|
换个角度来说, delete x; 只有当 property 是通过显式赋值创建时才抛出错误——this.x = 1;.如果 property 是通过变量声明创建的(var x = 1;),删除只是简单地从不执行,并且delete 正确地返回false. 如果property 是通过未声明赋值创建的(x = 1),删除按预期方式运行.
我是 在9月份反馈这个问题 的,Garrett Smith建议在IE中 “The global variable object被实现为一个 JScript 对象,并且 global object 是由 host 来实现的".Garrett 建议参考Eric Lippert的博客 .我们可以通过执行一些测试来证实了这一理论. 注意, this 和 window 指向同一个对象(我们可以采用 === 操作符来测试),但是 Variable 对象(声明函数时的那个) 与 this 引用不同.
1
2
3
4
5
6
7
|
/* 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
|
5. 误解
对于事物以及其运行方式和原理的美丽的理解是不能被低估的.我看到网上有一些没有真正理解 delete 操作而造成的一些误解.例如, 这是在 Stackoverflow 的一个答案(高得惊人的评级),自信地解释 “当删除的目标不是一个对象的 property 时,delete 应该是无操作”.现在我们理解了 delete行为的核心,就很清楚地知道,这个答案是相当不准确的.delete不区分变量和属性(事实上,对于delete来说,这些都是引用),并且只关心DontDelete 特性(property 要存在).看看误解彼此反弹也是有趣的,在同一 thread 中有人首先建议删除变量(当然是行不通的,除非是在 eval 中声明),然后另一个人提供了 一份错误的修正 可以如何在全局代码中删除变量而不是Function 代码中.
请小心使用Web上对 Javascript 的解释,在理想的情况下,应该去寻求理解问题的核心;)
6. 'delete' 与 host 对象(宿主对象?)
对 delete 的一个运算法则大概是这样的:如果操作数不是一个引用,返回true如果对象没有这个name的direct property,返回true(正如我们现在所知道的,对象可以是Activation 对象或 Global 对象)如果 property 存在,但具有DontDelete特性,返回 false其他情况,删除 property 并返回 true然而, delete 操作符与 host 对象的行为可能是相当难以预测的.其实并没有哪里出错: (规范中允许) host 对象对 operations 实现任何形式的行为,如 read (内部[[Get]]方法), write (内部[[Put]]方法)或 delete (内部[[Delete ]]方法),以及其他的一些操作. 对于自定义[[Delete]]的行为正是 host 对象如此混乱的原因.
我们已经看到一些IE的古怪,删除某些特定对象(这显然是实现为 host 对象)会抛出错误. 某些版本的Firefox在尝试删除 window.location 时也会抛出.当涉及到 host 对象时,你也不能相信 delete 的返回值,看看在Firefox中会发生什么:
1
2
3
4
5
6
|
/* "alert" 是 `window` 的一个 direct property
(如果我们采信 `hasOwnProperty` 的话) */
window.hasOwnProperty(
'alert'
);
// true
delete window.alert;
// true
typeof window.alert;
// "function"
|
删除 window.alert 返回 true ,即使对该 property 没有什么理由导致这个结果. 它解析为一个引用(所以在第一部不能返回true). 这是window的一个 direct property(所以第二步不能返回true;).delete操作返回true的唯一途径 就是到达运算法则的第四步,并且真正地删除了一个属性. 是的,在这里 property 从来没有被删除.
这个故事的寓意就是 绝不能信任 host对象.
作为一根bonus,下面是IE中 delete 行为的一个怪异的 case :
1
2
3
4
5
6
7
8
|
var element = document.createElement(
'div'
)
delete element.onclick;
// throws "Object doesn't support this action"
document.body.x =
1
;
delete document.body.x;
// throws "Object doesn't support this action"
// in IE8
delete XMLHttpRequest.prototype.open;
// throws "Object doesn't support this action"
|
7. EcmaScript5严格模式
那么 ECMAScript5 的严格模式带来了什么? 介绍了一些限制. 当使用 delete 操作符来删除 变量,函数参数或函数标识符 的直接引用时,将会抛出 SyntaxError语法错误.此外,如果 property 内部[[Configurable]]== false时,会抛出 TypeError:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
(function(foo){
"use strict"
;
// 在此函数内使用 strict mode
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`属性拥有 { [[Configurable]] : false } */
delete (function(){}).length;
// TypeError
})();
|
1
2
|
"use strict"
;
delete i_dont_exist;
// SyntaxError
|
1
2
|
"use strict"
;
i_dont_exist =
1
;
// ReferenceError
|
在严格模式下,不会默默地忽略删除,而是需要更多 激进的和 基础的(descriptive)措施.
8. 小结
这篇文章非常的长,所以我不再去讨论诸如使用 delete 来移除数组项(array items)及其含义.你可以参考 MOZILLA对于delete的详细说明(或自己搜索与实验).下面是关于Javascript中删除机制的一个简短摘要:
变量和函数声明都是 Activation 或 Global 对象的 properties.Properties 拥有 attributes特性,其中一个是 DontDelete,决定 property 是否可以被删除.在全局和Function代码中声明的变量和函数,创建的properties 总是被赋予 DontDelete 标志.函数 arguments 也是 Activation 对象的properties ,并在创建时赋予 DontDelete 标志.在Eval代码中声明的变量和函数在创建 properties 时没有DontDelete标志.新指定的properties 创建时标志位是 empty 的(所以没有DontDelete标志).Host 对象自己决定是否允许被删除.如果你想了解更多信息,请参阅: ECMA-262规范_en.pdf
我希望你喜欢这个概述并学到一些新东西.一如既往地欢迎任何问题,建议和修正.