深入理解JS的delete
原文链接: Understanding delete
原文作者: Kangax
原文日期: 2010年01月10日
翻译日期: 2014年02月07日
翻译人员: 铁锚!!!!!!!!草稿版本的翻译完成以后,无意中搜索发现了另外一篇翻译,也是同一篇原文,比我翻译的好。 justjavac,迷渡是我很尊敬的一位前辈, 链接如下:javascript 中的 delete
虽然说花了2个星期,进度也不理想,但是至少知道自己还有很多差距,也算是值得的。
教训就是以后做翻译绝对要先搜索一些关键字.
说明: 在本文中,将 property 统一翻译为属性,而将 attribute翻译为特性. 比如eval.length,其中 length 就是 eval函数(引用)的一个属性,但是 length这个属性又拥有一些特性,比如 不可删除,不可修改等.几个星期前,我有幸阅读了 斯托诺夫(Stoyan Stefanov) 的 Object-Oriented Javascript 一书.(原文所引的是第一版,现在是第二版,错误应该已经得到修正.)
这本书在亚马逊得到非常高的评价(现在41个评论与5星推荐),所以我就很好奇,看看是不是值得推荐.我从 functions 的章节开始阅读,并非常喜欢书里对事物的解说方式;书中的示例有很合理的结构顺序以及先进的组织方式,即使是初学者也能很容易了解和掌握.然而,几乎很快,我在章节中偶然发现了一个有趣的误解 —— 关于functions 的删除.当然也还有一些其他的错误(比如函数声明和函数表达式的区别),但在本文中我们不谈论其他.
书里说 ”function 被视为正常的变量 - 可以复制到另一个变量,甚至可以被删除.” 在这个解释后面,有一个例子:
>>> var sum = function(a, b) {return a + b;} >>> var add = sum; >>> delete sum true >>> typeof sum; "undefined"忽略缺少的分号,你能发现这段代码有什么问题吗?当然,问题是,删除 sum 变量不应该成功; delete 语句不应返回 true ,而且 typeof sum 也不应返回“undefined”.因为在 Javascript 中删除变量是不可能的.至少在这种声明方式下不能.
那为什么此示例会出错? 这是一个错误?玩笑?应该不是.整个代码片段实际上是 Firebug控制台 的输出, Stoyan 肯定是快速测试过的.原因是Firebug好像采用了一些不同的删除规则. 所以是 Firebug 导致 Stoyan 在这里出错!但这到底是怎么回事呢?
要回答这个问题,我们需要了解在Javascript中 delete操作符的工作机制: 什么可以被删除,什么不能被删除以及为什么.现在我将试图详细解释其原因.我们将发现 Firebug “怪异”的行为并认识到并不是所有都是怪异的,我们将深入研究当声明变量,functions,指定属性和删除它们 时在幕后究竟发生了什么; 我们将列举浏览器的承诺和一些最臭名昭著的bug;我们也会讨论第五版 ECMAScript的严格模式,以及它如何改变delete操作符的行为.
在本文中 Javascript和ECMAScript 都指的是ECMAScript(除非特别指出是Mozilla的Javascript实现).
不出所料,网络上关于 delete 的解释相当稀少. MDC的文章 可能是最全面的资源,但遗憾的是错过了一些有趣的细节; 奇怪的是,这些被遗忘的事情之一正是Firebug的复杂行为的原因.而 MSDN参考手册 几乎是无用的.
1. 基本原理
我们可以删除对象的某个属性: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.
要理解这一点,我们首先需要理解变量实例化和 property 属性等概念——不幸的是在Javascript的书中很少涵盖这些东西.我会在接下来的几个段落简略地介绍这些.这些概念一点都不难理解!如果你不关心为什么JavaScript工作的方式会如此,请跳过这一章.
1.1 可执行代码的分类
在 ECMAScript 中有3种类型的可执行代码: 全局代码, 函数代码, 以及 Eval 代码.这些类型是自描述的,下面是一个简短的概述:
- 当一个文本 source 被当做作一段程序,它在全局范围内执行,被认为是全局代码(Global code).在浏览器环境中, SCRIPT 元素的内容通常被解析为程序,因此等价于全局代码.
- 直接在函数内执行的东西,很明显,被认为是一段函数代码(Function code).在浏览器中,事件属性的内容(例如 <p onclick = "…" > )通常被解析并被认为是一段函数代码.
- 最后,在内置 eval 函数中的文本被解析为 Eval code. 我们很快就会看到为什么这种类型是特殊的.
1.2 执行上下文
当 ECMAScript 代码执行时,它总是处于特定的执行上下文中的.执行上下文是一个抽象的存在,这有助于理解 scope 和 变量实例 是如何工作的的. 对于三种类型的可执行代码,每种都有一个执行上下文.当一个函数执行时,可以说被控制着进入 Function代码执行上下文;当全局代码执行时,进入全局代码的执行上下文 等等.正如您所见到的,执行上下文在逻辑上形成一个堆栈.首先是全局代码及其执行上下文;而全局代码可以调用一个函数,有函数自己的执行上下文,该函数可以调用另一个函数,等等等等.即使函数递归地调用其本身,每一次调用也会进入一个新的执行上下文.
1.3 Activation对象/Variable对象
每个执行上下文都有一个被叫做 Variable object (活化对象?)的对象与其相关联.类似于执行上下文,Variable 对象也是一个抽象的存在,用来描述变量实例化的一种机制.现在,有意思的是,在一个源文本中声明的变量和函数中实际上都被添加为该 Variable object 对象的属性(properties).当进入全局代码执行上下文,全局对象(Global object,如浏览器中的 window)被当做其 Variable object 对象.这正是为什么在全局范围内声明的变量或函数会成为全局对象的属性的原因:
/* 当在全局范围时, `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; // trueOK,全局变量成为了全局对象的属性,但局部变量(在函数内部声明)又是如何处理的呢?实际上是非常相似的: 他们成为 Variable 对象的属性(properties).唯一的区别是,当在函数代码中时,Variable 对象并不是全局对象,而是一个称为Activation object 的对象.每次进入函数的执行上下文时都会创建一个 Activation object 对象.
不仅函数内部声明的变量和函数会成为 Activation object 对象的属性;而且函数的每个参数(对应到相应的参数名)、以及一个特殊的 Arguments 对象(名为 arguments )也会成为 Activation object 对象的属性.注意, Activation 对象只是一种内部的机制,程序永远不可能真正地访问(引用)到这个对象.
(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);最后,在 Eval 代码内声明的变量被创建为调用上下文 Variable object 对象的属性.简言之,Eval代码在哪里被调用,内部的变量就相当于在哪里被声明:
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属性,而其他的不能删除的本质原因:
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 标志:(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" })();对应于 function参数的 properties 创建时也被赋予了 DontDelete 标志,所以也不能被删除:
(function(foo, bar){ delete foo; // false foo; // 1 delete bar; // false bar; // ‘blah‘ })(1, ‘blah‘);
1.6 未声明赋值
您也许还记得,未声明的赋值(没有 var 定义)将在全局对象上创建一个属性.除非是在查找到全局范围对象之前就已经在作用域链上发现了一个同名的属性.现在我们知道了 property 赋值和变量声明 的区别 —— 后者被设置了 DontDelete标志,而前一个没有被设置 —— 为什么未声明的赋值 创建的property是可删除的现在就很明显了(没设置 DontDelete标志):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"注意, attributes 是在 property 创建期间决定的(即便什么也没有设置).以后的赋值不会修改已存在property的attributes.理解这个区别是很重要的.
/* `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 标志的:eval(‘var foo = 1;‘); foo; // 1 delete foo; // true typeof foo; // "undefined"同样,当在函数内部调用Eval代码时:
(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声明被 提到变量声明之后,所以...):function x(){ } var x; // 只声明 typeof x; // "function" function a(){ } var a = 6; // 声明 + 赋值 typeof a; // "number"注意,函数声明优先并覆盖same-named(同名的)变量(或者,换句话说,Variable 对象的同一个property ).这是因为 function 声明在变量声明之后 实例化,并允许覆盖它们.函数声明不仅取代 property的前一个值,它也取代该 property 的特性(attributes).如果我们通过eval声明 function,该函数也应该用自己的替换该 property的attributes .由于在eval中声明的变量所创建的 properties 没有DontDelete标志,实例化这个新函数本质上应该移除property 现有的DontDelete属性,以使 移除property 可以被删除(当然,肯定也要让其指向新创建的函数).
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 是通过变量或函数声明 而创建的: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)令人惊讶的是,Internet Explorer 5.5 - 8 完全通过了测试套件的测试,除了删除非引用(例如 detele 1;)会抛出错误以外(就像在更古老的Safari中一样).但在IE中实际上有更严重的bugs,这些bug不会立即显示出来.这些缺陷是与全局对象有关的.
4. IE的Bugs
整篇文章只有Internet Explorer中有BUG吗?多么的意想不到啊!在IE(至少IE6 - IE8),以下表达式将会抛出错误(在Global code中执行时):
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)现在,与人们的预料相反,未声明的赋值(应该在全局对象上创建一个property )在IE中却创建可删除的properties:
x = 1; delete x; // true typeof x; // "undefined"但如果你想在全局代码中通过 this引用来删除这个property (delete this.x),那么一个你熟悉的错误就会冒出来:
x = 1; delete this.x; // TypeError: Cannot delete ‘this.x‘如果我们对这种行为进行推理,会发现在全局代码执行 delete this.x; 永远不会成功.当 property 是通过显式赋值创建的(this.x = 1;),delete将会抛出一个错误;当 property 是通过未声明赋值创建的(x = 1)或通过变量声明创建(var x = 1),delete将会抛出另一个错误.
换个角度来说, 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 引用不同.
/* 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
我们已经看到一些IE的古怪,删除某些特定对象(这显然是实现为 host 对象)会抛出错误. 某些版本的Firefox在尝试删除 window.location 时也会抛出.当涉及到 host 对象时,你也不能相信 delete 的返回值,看看在Firefox中会发生什么:
/* "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 :
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:(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 })();此外,删除未声明的变量(或者换句话说,未解决的引用)也会抛出语法错误:
"use strict"; delete i_dont_exist; // SyntaxError这有点类似于在严格模式下的未声明赋值的行为(除了抛出的是 ReferenceError 而不是SyntaxError以外):
"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 对象自己决定是否允许被删除.
我希望你喜欢这个概述并学到一些新东西.一如既往地欢迎任何问题,建议和修正.
郑重声明:本站内容如果来自互联网及其他传播媒体,其版权均属原媒体及文章作者所有。转载目的在于传递更多信息及用于网络分享,并不代表本站赞同其观点和对其真实性负责,也不构成任何其他建议。