javascript中的delete使用详解 |
在这篇文章中作者从《JavaScript面向对象编程指南》一书中关于 delete 的错误讲起,详细讲述了关于 delete 操作的实现, 局限以及在不同浏览器和插件(这里指 firebug)中的表现 。 下面翻译其中的主要部分 。 ...书中声称 “函数就像一个普通的变量那样——可以拷贝到不同变量,甚至被删除” 并附上了下面的代码片段作为说明: 复制代码 代码如下: >>> 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! 那么这里面究竟是怎么回事呢? 为了回答这个问题,我们需要了解 delete 运算符在 Javascript 中是如何工作的: 哪些可以被删除,哪些不能删除以及为什么 。 下面我试着解释一下这方面的细节 。 我们将通过观察 firebug 的“奇怪”的表现而认识到它实际上完全不“奇怪”; 我们将深入了解那些,当我们声明变量、函数,赋值属性和删除它们时的,隐藏在背后的细节; 我们将看一下浏览器对此的实现和一些有名的 bug; 我们还会讨论到 ECMAScript 版本 5 中的严格模式(strict mode)以及它如何改变 delete 运算符的行为 。 我在下面交替使用的 Javascript 和 ECMPScript 一般都指 ECMAScript(除非当明确谈到 Mozilla 的 JavaScript™ 实现时) 。 复制代码 代码如下: var x = { a: 1 }; delete x.a; // true x.a; // undefined 但却不能删除一个变量: 复制代码 代码如下: var x = 1; delete x; // false; x; // 1 也不能删除一个函数: 复制代码 代码如下: function x() {}; delete x; // false; typeof x; // "function" 注意:delete 只有当一个属性无法被删除时才返回 false 。 为了理解这一点,我们需要首先把握一些概念: 变量实例化(variable instantiation)和属性的内部属性(property attributes) (译者按:关于 property 和 attributes 的区别见参考文章,根据下面涉及到的内容,拟译成内部属性) ——这些很少在 javascript 书中被提到 。 在下面几段中我将试着简短地回顾这些内容,要理解它们并不难 。 如果你并不关注它们表现背后的原因,可以跳过这一章 。 1.1、代码的类型 | Type of code ECMAScript 中有三类可执行代码: 1.全局代码 Global code 这几类的含义大致就像它们命名的那样,但还是快速地回顾一下: 1.当一个源文件被看做是一个程序,它在全局作用域(scope)内执行,而这就被认为是一段全局代码 Global code 。 在浏览器环境下,SCRIPT 元素的内容通常都被解析为一个程序,因而作为全局代码来执行 。 2.当然,任何在一段函数中直接执行的代码就被认为是一段函数代码 Function code, 在浏览器环境下,事件属性的内容(e.g. <a onclick="...")通常都作为函数代码来解析和执行 。 3.最后,放入内建函数 eval 中的代码就作为 Eval code 来解析 。 我们将很快看到为什么这一类型是特殊的 。 1.2、代码执行的上下文 | Execution Context 正如你所见,执行上下文在逻辑上是一个栈(stack) 。 首先可能有一段全局代码,它拥有属于自己的执行上下文; 在这段代码中可能调用一个函数,这个函数同样拥有属于自己的执行上下文; 这个函数可能调用另一个函数,等等 。 即使当函数递归调用自己时,在每一步调用中仍然进入了不同的执行上下文 。 1.3、活化对象和变量对象 | Activation object / Variable object 当控制进入了全局代码的执行上下文时,一个全局对象被用作变量对象 。 这恰恰是为什么全局声明的变量和函数变成一个全局对象的属性的原因: 复制代码 代码如下: var GLOBAL_OBJECT = this; var foo = 1; GLOBAL_OBJECT.foo; // 1 function bar() {}; typeof GLOBAL_OBJECT.bar; // "function" GLOBAL_OBJECT.bar === bar; // true Ok, 所以全局变量成了全局函数的属性,那么局部变量——那些在函数代码(Function code)中声明的变量呢? 事实上那很简单:他们也成了变量对象的属性 。 唯一的区别是,在函数代码中,变量对象不是一个全局对象, 而是一个我们称之为活化对象(Activation object) 。 每次进入函数代码的执行上下文时都会创建一个活化对象 。 并非只有在函数代码中声明的变量和函数才成为活化对象的属性: 函数的每一个实参(arguments,以各自相对应的形参的名字为属性名), 以及一个特殊的Arguments对象(以arguments为属性名)同样成为了活化对象的属性 。 需要注意的是,活化对象作为一个内部的机制事实上不能被程序代码所访问 。 复制代码 代码如下: (function(foo) { var bar = 2; function baz() {}; /* 在抽象的过程中, 特殊的arguments对象变成了所在函数的活化对象的属性: ACTIVATION_OBJECT.arguments = arguments; ...参数foo‘也是一样: ACTIVATION_OBJECT.foo; // 1 ...变量bar也是一样: ACTIVATION_OBJECT.bar; // 2 ...函数baz也是一样: typeof ACTIVATION_OBJECT.baz; // "function" */ }) (1); 最后,Eval code 中声明的变量成为了上下文的变量对象(contexts Variable object)的属性 。 Eval code 简单地使用在它调用中的执行上下文的变量对象 。 复制代码 代码如下: var GLOBAL_OBJECT = this; eval(var foo = 1); GLOBAL_OBJECT.foo; // 1; (function() { /* 1.4、属性的内部属性 | Property attributes 当声明变量和函数时,它们成为了变量对象(Variable object)——要么是活化对象(在函数代码中), 要么是全局对象(在全局代码中)——的属性,这些属性伴随生成了内部属性 DontDelete 。 然而,任何显式/隐式赋值的属性不生成 DontDelete 。 而这就是本质上为什么我们能删除一些属性而不能删除其他的原因 。 复制代码 代码如下: var GLOBAL_OBJECT = this; /* foo是全局对象的一个属性, /* bar是全局对象的一个属性, /* baz也是全局对象的一个属性, 1.5、内建和DontDelete | Build-ins and DontDelete 所以这就是所有这一切发生的原因:属性的一个特殊的内部属性控制着该属性是否可以被删除 。 注意:内建对象的一些属性拥有内部属性 DontDelete,因此不能被删除; 特殊的 arguments 变量(如我们所知的,活化对象的属性)拥有 DontDelete; 任何函数实例的 length (返回形参长度)属性也拥有 DontDelete: 复制代码 代码如下: (function() { //不能删除arguments,因为有DontDelete delete arguments; // false; typeof arguments; // "object" //也不能删除函数的length,因为有DontDelete 与函数 arguments 相关联的属性也拥有 DontDelete,同样不能被删除 复制代码 代码如下: (function(foo,bar) { delete foo; // false foo; // 1 delete bar; // false 1.6、未声明的变量赋值 | Undeclared assignments 复制代码 代码如下: var GLOBAL_OBJECT = this; /* 通过变量声明生成全局对象的属性,拥有DontDelete */ /* 通过未声明的变量赋值生成全局对象的属性,没有DontDelete */ delete foo; // false /* foo创建的同时生成DontDelete */ /* 之后的赋值过程不改变已有属性的内部属性,DontDelete仍然存在 */ /* 但赋值一个不存在的属性时,创建了一个没有内部属性的属性,因此没有DontDelete */ 二、Firebug 的混乱 | Firebug confusion 那么, firebug 中发生了什么? 为什么在控制台中声明的变量能够被删除,而不是想我们之前讨论的那样? 我之前说过,Eval code 在它处理变量声明时有一个特殊的行为: 在 Eval code 中声明的变量事实上生成一个没有 DontDelete 的属性 。 复制代码 代码如下: eval(var foo = 1;); foo; // 1 delete foo; // true typeof foo; // "undefined" 在函数代码中也是一样: 复制代码 代码如下: (function() { eval(var foo = 1;); foo; // 1 delete foo; // true typeof foo; // "undefined" }) (); 而这就是 Firebug 中异常行为的原因了 。 所有在控制台中的调试文本似乎是以 Eval code 来编译和执行的,而不是在全局或函数代码中执行 。 显然,其中的变量声明最终都生成了不带 DontDelete 的属性,所以可以被删除 。 所以要小心普通的全局代码和 Firebug 控制台中代码的区别 。 2.1、通过eval删除变量 | Delete variables via eval 复制代码 代码如下: function x() { }; var x; typeof x; // “function” 那么为什么函数声明拥有优先权而能覆盖同名变量(或者换句话说,变量对象(Variable object)的相同属性)呢? 这是因为函数声明的实例化过程在变量声明之后,因此可以覆盖它们 。 (译者按:函数声明只能覆盖声明而未赋值的同名变量,如果在声明时赋值了值(e.g. var x = 1)则赋值值的过程在函数初始化之后,函数声明反而被变量赋值所覆盖,如下:) 复制代码 代码如下: var x = 1; function x() { }; typeof x; // "number" 函数声明不止替换了属性的值,同时也替换了它的内部属性 。 如果我们通过 eval 来声明函数,这一函数也将用它自己的内部属性来替换之前的 。 而由于在 eval 中声明的变量生成的属性没有 DontDelete, 实例化这个函数将在“理论上”移除原属性已有的 DontDelete 内部属性, 而使得这一属性可以删除(当然,同时也将值指向了新生成的函数) 。 复制代码 代码如下: var x = 1; /*不能删除,‘x拥有DontDelete*/ delete x; // false typeof x; // "number" eval(function x() { }); 不幸的是,这种欺骗技术在我尝试的各个浏览器中都没有成功 。 这里我可能错过了什么,或者这个行为太隐蔽而以至于各个浏览器没有注意到它 。 (译者按:这里的问题可能在于:函数声明和变量声明之间的覆盖只是值指向的改变, 而内部属性 DontDelete 则在最初声明处确定而不再改变,而 eval 中声明的变量和函数,也只是在其外部上下文中未声明过的那部分才能被删除 。 关于执行顺序,由于 eval 作为函数,它的调用永远在其外部上下文中其他变量和函数声明之后, 因此相关的内部属性也已确定,覆盖的只是值的指向 。如下:) 复制代码 代码如下: /* 第一个 alert 返回 “undefined”,因为赋值过程在声明过程和eval执行过程之后; 第二个alert返回 “false”, 因为尽管x声明的位置在eval之后, 但是eval的执行却在变量声明之后,因此已无法删除 */ eval( alert( x ); alert(delete x) ); var x = 1; 三、浏览器的遵守情况 | Browsers compliance 我写了一个简单的测试单元来检查全局代码、函数代码和Eval代码的遵守情况 。 测试单元同时检测了 delete 操作的返回值和属性是否像预期那样被删除 。 delete 的返回值并不像它的实际结果那样重要,delete 操作返回 true 或 false 并不重要, 重要的是拥有/没有 DontDelete 的属性是否被删除 。 现代浏览器总的来说还是遵守删除规则的,以下浏览器全部通过测试: Opera 7.54+, Firefox 1.0+, Safari 3.1.2+, Chrome 4+ 。 Safari 2.x 和 3.0.4 在删除函数 arguments 时存在问题,似乎这些属性在创建时不带 DontDelete,因此可以被删除 。 Safari 2.x 还有其他问题——删除无引用时(例如delete 1)抛出错误(译者按:IE 同样有); 函数声明生成了可删除的属性(奇怪的是变量声明则正常); eval 中的变量声明变成不可删除(而 eval 中的函数声明则正常) 。 与 Safari 类似,Konqueror(3.5,而非4.3)在 delete 无引用和删除 arguments 是也存在同样问题 。 3.1、Gecko DontDelete bug 复制代码 代码如下: function foo() { }; delete foo; // false; typeof foo; // "function" this.foo = 1; 令人惊讶的是,IE5.5-8 也通过了绝大部分测试,除了删除非引用抛出错误(e.g. delete 1,就像旧的 Safari) 。 但是,虽然不能马上发现,事实上 IE 存在更严重的 bug,这些 bug 是关于全局对象 。 四、IE bugs 在 IE 中(至少在 IE6-8 中),下面的表达式抛出异常(在全局代码中): 复制代码 代码如下: this.x = 1; delete x; // TypeError: Object doesnt support this action 而下面则是另一个: 复制代码 代码如下: var x =1; delete this.x; // TypeError: Cannot delete this.x // 译者按:在IE8下抛出此异常,在IE6,7下抛出的是和上面一样的异常 这似乎说明,在 IE 中在全局代码中的变量声明并没有生成全局对象的同名属性 。 通过赋值创建的属性(this.x = 1)然后通过 delete x 删除时抛出异常; 通过变量声明(var x = 1)创建的属性然后通过 delete this.x 删除时抛出另一个(译者按:在 IE6,7 下错误信息与上面的相同) 。 但不只是这样,事实上通过显式赋值创建的属性在删除时总是抛出异常 。 这不只是一个错误,而是创建的属性看上去拥有了 DontDelete 内部属性,而按规则应该是没有的: 复制代码 代码如下: this.x = 1; delete this.x; // TypeError: Object doesnt support this action delete x; // TypeError: Object doesnt support this action 另一方面,未声明的变量赋值(那些同样生成全局对象的属性)又确实在IE下能够正常删除: 复制代码 代码如下: x = 1; delete x; // true 但如果你试图通过 this 关键字来进行删除(delete this.x),那么上面的异常又将抛出: 复制代码 代码如下: x = 1; delete this.x; //TypeError: Cannot delete this.x 如果归纳一下,我们将发现在全局代码中‘delete this.x永远不会成功 。 当通过显式赋值来生成属性(this.x = 1)时抛出一个异常; 当通过声明/非声明变量的方式(var x = 1 or x = 1)生成属性时抛出另一个异常 。 而另一方面,delete x 只有在显示赋值生成属性(this.x = 1)时才抛出异常 。 在 9 月我讨论了这个问题,其中 Garrett Smith 认为在 IE 中全局变量对象(Global variable object)实现为一个 JScript 对象,而全局对象则由宿主对象实现 。 我们能通过几个测试来在某种程度上确认这一理论 。 注意,this 和 window 似乎引用同一个对象(如果 ‘===运算符可以信任的话), 而变量对象 Variable object (函数声明的基础)则与 this 引用的不同 。 复制代码 代码如下: function getBase() { return this; }; getBase() === this.getBase(); // false 五、误解 | Misconceptions 我们不能低估理解事物工作原理的重要性 。 我看过网络上一些关于 delete 操作的误解 。 例如,Stackoverflow 上的一个答案(而且等级还很高),里面解释说“delete is supposed to be no-op when target isnt an object property” 。 现在我们了解了 delete 操作的核心,也就清楚了这个答案是不正确的 。 delete 不区分变量和属性(事实上在 delete 操作中这些都是引用),而只关心 DontDelete(以及属性是否已经存在) 。 六、delete和宿主对象 | delete‘ and host object 一个 delete 的算法大致像这样: 1. 如果运算元(operand)不是引用,返回 true 我们已经看到了在IE中的一些问题:当删除某些对象(那些实现为了宿主对象)属性时抛出异常 。 一些版本的 firefox 当试图删除 window.location 时抛出异常(译者按:IE 同样抛出) 。 同样,在一些宿主对象中你也不能相信 delete 的返回值, 例如下面发生在 firefox 中的(译者按:chrome 中同样结果;IE 中抛出异常;opera 和 safari 允许删除,并且删除后无法调用,姑且算正常‘,尽管,从下面的讨论来看似乎却是不正常的,它们事实上删除了不能删除的属性,而前面的浏览器没有): 复制代码 代码如下: /* alert是window‘的一个直接属性(如果我们能够相信hasOwnProperty) */ window.hasOwnProperty(alert); // true delete window.alert; // true delete window.alert 返回 true,尽管这个属性没有任何条件可能产生这个结果(按照上面的算法): 它解析为一个引用,因此不能在第一步返回 true; 它是 window 对象的直接属性,因此不能在第二步返回 true; 唯一能返回 true 的是当算法达到最后一步同时确实删除这个属性,而事实上它并没有被删除 。 (译者按:不,在 opera 和 safari 中确实被删除了...) 。 所以这个故事告诉我们永远不要相信宿主对象 。 七、ES5 严格模式 | ES5 strict mode 那么 ECMAScript 第 5 版中的严格模式将带来什么? 目前介绍了其中的一些限制 。 当删除操作指向一个变量/函数参数/函数声明的直接引用时抛出 SyntaxError 。 此外,如果属性拥有内部属性[[Configurable]] == false,将抛出 TypeError: 复制代码 代码如下: (function(foo) { "use strict"; //在函数中开启严格模式 var bar; /* function实例的length拥有[[Configurable]] : false */ 而且,在严格模式下,删除未声明的变量(换句话说,未解析的引用),同样抛出 SyntaxError; 与它类似的,相同模式下未声明的赋值也将抛出异常(ReferenceError) 复制代码 代码如下: "use strict"; delete i_dont_exist; // SyntaxError i_dont_exist_either = 1; // ReferenceError 看了之前给出的变量、函数声明和参数的例子,相信现在你也理解了,所有这些限制都是有其意义的 。 严格模式采取了更积极的和描述性的措施,而不只是忽略这些问题 。 八、总结 | Summary 由于这篇文章已经很长了,因此我就不再讨论另一些内容(e.g.通过 delete 删除数组项及其影响) 。 你可以翻阅 MDC/MDN 上的文章或阅读规范然后自己测试 。 下面是关于 Javascript 中 delete 如何工作的一个简单的总结: •变量和函数声明都是活化(Activation)全局(Global)对象的属性 。 原文:Understanding delete译文:javascript 中的 delete译者:justjavac |