深入理解Delete(JavaScript)
Delete 众所周知是删除对象中的属性. 但如果不深入了解delete的真正使用在项目中会出现非常严重的问题 (:
Following 是翻译 kangax 的一篇文章 "Understanding Delete";
PS:文章主要分为8部分, 有时间弄点瓜子儿, 整壶茶了解一下. (小编建议直接看原文地址, 以下翻译仅供自己学习使用);
相信大家如果有时间看完会有收获...也希望有大牛能指出其中翻译的不足...
目录:
§ Activation object / Variable object
§ Build-ins and DontDelete(嵌入式/不可删除)
§ Undeclared assignments (未声明任务)
§ FireBug confusion (奇异的FireBug)
§ Deleting variables via eval(通过eval删除变量)
§ Browsers compliance (浏览器兼容性)
================Enein翻译===================
先上例子:
>>> var sum = function(a, b) {return a + b;} >>> var add = sum; >>> delete sum true >>> typeof sum; "undefined"
忽略几个丢失分号. 这段代码你能看出什么问题?
当然这个问题很明显 "delete sum" 是不会成功的. delete 返回的值不应该是 "true" , "typeof sum" 返回的结果也不是"undefined"。造成问题的原因是 在JavaScript中"delete 是不可以删除变量的".
这个例子有问题? 排版问题? 是个变相题? 应该都不是. 上面的代码会在FireBug Console下正确输出.(你可以快速测试一下) 仿佛在FireBug下有它自己的删除规则. 这...给我干蒙了. 到底是怎么回事? 我们来讨论一下.
要想知道答案我们首先要先知道 "delete" 操作符在JavaScript中的实际是怎样工作的: (主要从3个方向 什么情况能正确删除, 什么时候不能删除, 为什么);
让我们带着疑问继续往下看:(I’ll try to explain this in details)
我们来看一个FireBug的古怪行为.并了解其实这是正常的.
我们将深入了解下 "声明变量","函数","加入属性"是怎么工作的并在适当的时候删除它们.我们还会看一下浏览器的兼容性以及其一些常见的Bugs, ECMAScript 5 strict mode 和如何改变delete 操作符的行为
注释: 在这里我将使用JavaScript和ECMAScript(这是真正意义上的ECMAScript除非有明确声明为Mozilla's ECMAScript扩展)
PS:这一段是作者对Mozilla MSN 和 MSDN 上的两篇文章发表的个人看法(他会认为practically useless)这里不做翻译有兴趣的同学可以点其链接自行查看分析.
为什么它能删除对象的属性:
var o = { x: 1 }; delete o.x; // true o.x; // undefined
变量却不能, like this:
var x = 1; delete x; // false x; // 1
函数也不允许, like this:
function x(){} delete x; // false typeof x; // "function"
注意当 属性不能被删除的时候将返回 false
要明白理解这些, 要需要进一步理解变量实例概念、属性的特性。(有限的是在JavaScript相关书籍中涉及的知识还是比较少的)以下就要详细的介绍.
(如果你不关心这些东西为什么工作方式是这样的,那就skip this chapter)
§ Type of Code (代码级别) [ps:代码级别是出于自己的理解]
在ECMAScript中有3种作用域: Global code(全局作用域), Function code(函数作用域), Eval code(Eval作用域) 以下对三种级别的描述.
Global code : 当一段文本做为一个程序的时候, 它是在全局作用域下执行的. 在浏览器环境中通常写在SCRIPT标签下的内容会被解析, 因为也算是全局作用域
Function code : 任何东西在function里是会随着function执行并执行.很明显这是属于函数作用域, 在浏览器中事件属性通过也会被当作函数作用域.(e.g <p onclick=""/>)
Eval code : 最后, 在eval函数体里的代码就是 Eval作用域.很快我们就会看到为什么这个类型是特殊的.
当ECMAScript的代码执行的时候, 它就一直在某一个执行上下文中, “Execution context 是一个抽象的实体” 它会使我们明白作用或和变量实例化的过程. 对以上中种类型的范围, 它们就是一个执行上下文.
当function被执行的时候, 这个实体的上下文就是"Function code", 当代码是在Global code下被执行的时候, 那么它就是 "Global code" , 也 so on.
就你像我们看到的那样, 执行上下文逻辑上属于一个 stack (栈) 首先它可能是执行在全局作用域下的, 它拥有自己的上下文, 在这段代码里, 可能还会调用一个function, 这个function也会有自己的上下文, 在这个function里有可能还会调用一个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)变量对象的属性. 只是在作为Function code的时候有所不同, 一个变量对象不是一个全局对象. 但它会调用一个"Activation object"(激活对象), 每一次给函数分配上下文的时候Activation object将会被创建.
不仅仅是变量和函数的声明会成为Activation object的属性, 函数的形参(形式参数:对应实际参数)和特殊对象Arguments 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 code中的变量声明是作为 创建变量对象上下文调用时的属性 Eval code 简单的使用变量对象的执行上下文, 代码执行是这样的:
var GLOBAL_OBJECT = this;
/*'foo' 被创建为一个变量对象调用上下文的属性, 在这个案例中它是全局对象*/ eval('var foo = 1;'); GLOBAL_OBJECT.foo; // 1 (function(){ /* 'bar' 被创建作为变量对象调用时上下文中的属性, 在案例中是function链中的激活对象*/ eval('var bar = 1;'); /* In abstract terms, ACTIVATION_OBJECT.bar; // 1 */ })();
我们就快要明白了, 现在我们清楚的明白变量到底发生了什么(它们属性之间的变换), 剩下 Property attributes了.每个属性都会存在0个或多个属性包括(ReadOnly(只读),DontEnum(不可枚举), DontDelete(不可删除)) 对于今天的话题我们只讨论DontDelete.
当声明变量和函数变成变量对象的属性或者一个激活对象(作为一个Function code), 或者全局对象(Global code), 这些属性被创建并含有DontDelete特性.无论怎么样, 一些显式(隐式)属性分配上也会创建不含有Dontdelete的属性 为什么有的能有的不能:
var GLOBAL_OBJECT = this; /* 'foo' 是全局对象属性 它被创建通过变量声明所以它存在DontDelete特性 这就是它为什么不会被删除 */ var foo = 1; delete foo; // false typeof foo; // "number" /* 'bar' 是一个全局对象的属性 它被创建通过函数声明所以它存在DontDelete属性 这也就是它为什么也删除不了 */ function bar(){} delete bar; // false typeof bar; // "function" /* ‘baz’ 也是全局对象的属性 它的创建是通过分配属性所以它没有DontDelete是可以删除的 */ GLOBAL_OBJECT.baz = 'blah'; delete GLOBAL_OBJECT.baz; // true typeof GLOBAL_OBJECT.baz; // "undefined"
§ Build-ins and DontDelete(嵌入式和不可删除)
这节我们说的是, 属性的一些特殊特性来控制这些属性可否被删除(注意: 一些内置的属性会被默认指定成DontDelete, 固不能被删除)特殊arguments变量(现在我们知道它是激活对象的属性)有DontDelete. 同样的一些function实例的length属性也存在DontDelete:
(function(){ /* 不能删除 'arguments', 它是不可删除的*/ delete arguments; // false typeof arguments; // "object" /* 不能删除function的lenth属性, 它也是不可删除的 */ function f(){} delete f.length; // false typeof f.length; // "number" })();
同样, 函数的形参也是有DontDelete的也是不可删除的.
(function(foo, bar){ delete foo; // false foo; // 1 delete bar; // false bar; // 'blah' })(1, 'blah');
§ Undeclared assignments (未声明的任务)
未声明的任务创建一个全局对象的属性. 除非在全局对象之前你能找到这个属性是属性哪个作用域链的. 现在我们清楚,属性任务和变量声明之间的不同, 后者是是DontDelete属性, 前者则不是(它应该清楚为什么未被声明的会创建不含有DontDelete的属性).
var GLOBAL_OBJECT = this; /* 创建全局属性通过变量声明; 属性不可删除的*/ var foo = 1; /* 创建全局属性通过未声明的任务 其属性是可删除的 */ bar = 2; delete foo; // false typeof foo; // "number" delete bar; // true typeof bar; // "undefined"
注意在属性创建期间, 其属性是被确定的. 后面的任务是不可改变已存在的属性的, 明白这点是很重要的.
/* 'foo' 作为含有DontDelete的属性被创建 */ function foo(){} /* 之后的任务不能修改其属性, DontDelete还在 */ foo = 1; delete foo; // false typeof foo; // "number" /* 但加入属性是新的, 就不含有DontDelete. */ this.bar = 1; delete bar; // true typeof bar; // "undefined"
§ FireBug confusion (奇异的FireBug)
在FireBug发生了什么? 之前说过在FireBug console中变量的声明是可以删除的. 这违背了我们之前说的所有? 好吧, 之前我说过, Eval code的变量声明时有着特殊的行为, 变量声明在Eval code里实际上是创建了没有DontDelete的属性:
eval('var foo = 1;'); foo; // 1 delete foo; // true typeof foo; // "undefined"
同样对于在Function code里调用:
(function(){ eval('var foo = 1;'); foo; // 1 delete foo; // true typeof foo; // "undefined" })();
这就是重点, 所有在Firebug console中执行的代码会被当成是 Eval code来进行解析 所以
和console的不同.
§ Deleting variables via eval(通过eval删除变量)
最有意思的是eval的特性, 另一方面ECMAScript能从技术上允许我们去删除不可删除的属性.在同一个上下文中function的声明是可以被同名变量重写的.
function x(){ } var x; typeof x; // "function"
注意 function声明优先,重写同名变量(或者, 换句话说, 在变量对象中存在了相同属性). 这是因为 函数声明被实例是在变量声名(Variable declarations)之后, 是允许被覆盖的不仅函数声明替换这前属性的值, 它也能替换它的属性.
如果我们通过eval来声明function那么还是可以替换相应的属性, 因为在eval里创建的变量声明没有DontDelete, 以下示例会从本质上删除存在的DontDelete特性。
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"
不幸的事, 我尝试各种不能工作的场景, 有可能我会有所疏漏.
§ Browsers compliance (浏览器兼容性)
学习这些东西的工作原理是很实用的, 实践至上. 在浏览器兼容上会存在多在的差异.作者做了很多的浏览器测试, 最主要的是属性中含有DontDelete是不可删除的,相反则然.
当今浏览器的脾气都是很友好的. 我测试的Opera 7.54+, Firefox 1.0+, Safari 3.1.2+, Chrome 4+浏览器都是可行的.
Safari 2.x and 3.0.4 是有问题的对于function的参数问题上;这些参数被看做没有DontDelete特性, 问题主要是我们可以detele它们. 实际上Safari 2.x存在更多的问题(删除没有引用的变量e.g delete 1)会抛异常, function的声明会创建可删除属性(不包括变量声名), 变量声明在eval中变为不可删除(除了function声明).
Konqueror (3.5)也同样(删除function参数会报错)
Gecko DontDelete Bug
Gecko 1.8.x browsers — Firefox 2.x, Camino 1.x, Seamonkey 1.x, etc. 显示出一个很有意思的bug “显式地设定一个属性是可以移除DontDelete特性, 即使这个属性通过变量或函数声明被创建”:
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)
比较出乎意外的是IE 5.5 - 8 基本测试都通过了. 只是删除没有引用的会报错(e.g delete 1) , 但其实实际上IE中有更严重的BUG是关于全局对象的.
IE BUGS(IE bug)
这一章主要是说一下Internat Explorer下的BUGS:
在IE5.5-8, 下面的代码会报错(在全局域中执行)
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删除也会报错.
但说的也不全部, 显示的创建一个属性在删除的时候是一直会报错的
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)
现在, 相反 未声明的任务(将会在全局对象上)会创建可删除属性在IE里:
x = 1; delete x; // true typeof x; // "undefined"
但你要尝试使用全局对象的属性的方式来删除, 则会报错:
x = 1; delete this.x; // TypeError: Cannot delete 'this.x'
小结: 只要是 delete this.x 都不会成功.
Misconceptions (歧义)
/*...*/
§ 'delete' and host objects
简单的推算一下delete如下:
- 如果这个运算对象没有引用, 返回 true.
- 如果不是Object的内部属性, 返回 true.
- 如果Object有属性但有DontDelete特性, 返回 false.
- 其它移除属性返回 true.
无论怎么, delete操作符在宿主对象上的行为也是不可预知的 这是其实是没有问题的, 宿主对象是允许(通过规范)去实现各种操作行为比如 read(内部实现[[Get]]方法), write(内部实现[[Put]]方法), delete(内部实现[[Delete]]方法).
之前我们已经讨论了IE的差异, delete 某一对象会抛异常, 在一些火狐版本中删除 window.location 抛出的异常, 你是不能相信delete 宿主对象的属性的返回值的. 看下面代码在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"
删除window.alert返回的是true , 它的解析过程 :
One step : 被解析成一个引用(不会返回true);
two step : 是window的内部属性(不会返回true);
只有在真正 "delete window.alert" 的时候才真正删除了嘛? no 它还是没有被删除.
小结: 从来不要相信宿主对象
ECMAScript 规范 严格格式下会有很多限制, 在这几种情况下会报语法错误: delete 直接去删除 变量, 函数的参数, 函数定义, 另外, 当属性有内部属性[[Configurable]] == false 是会报 类型错误:
(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 })();
另外, 在删除未声明变量(或未指明的引用)抛出词法错误:
"use strict"; delete i_dont_exist; // SyntaxError
同样的在未指定确定类型的变量在严格模式(strict mode)下也是会报词法错误的:
"use strict"; i_dont_exist = 1; // ReferenceError
现在我明白了, 在严格模式下的这些限制都是很有用的, ECMAScript strict mode 解决了很多问题, 而不是忽视它们, 从中我们也可以通过ECMAScript这些限制, 反向理解来学习更深入的知识.
这篇文章说的太长了, 如果你能静下心来好好看完, 那你将会明白很多, 这里我只是说了一部分关于Array的delete我这里就不说了, 但希望有兴趣的同学可以自己去尝试(你可以参考MDC for that particular explanation 文章).
这是里简单的做一下在JavaScript中delete的操作:
-
变量和函数声明属性要么是激活对象, 要么是全局对象
- 属性里有DontDelete特性的表示不可删除属性.
-
变量和函数声明只要是在"全局代码块"/"函数级代码块"中都会有 —— DontDelete.
- Functions的参数也是属于激活对象的属性, 所以也有 —— DontDelete.
-
变量和函数声明在Eval代码块中的, 都不会创建 —— DontDelete.
- 为对象加入新的属性(没有任何特性), 也是不会创建 —— DontDelete.
-
不管他们想怎样, 宿主对象对删除是会返回状态的.
================Enein翻译===================