深入理解Delete(JavaScript)

深入理解Delete(JavaScript)

 

Delete  众所周知是删除对象中的属性. 但如果不深入了解delete的真正使用在项目中会出现非常严重的问题 (:

Following 是翻译  kangax 的一篇文章 "Understanding Delete";


PS:文章主要分为8部分, 有时间弄点瓜子儿, 整壶茶了解一下. (小编建议直接看原文地址, 以下翻译仅供自己学习使用);

相信大家如果有时间看完会有收获...也希望有大牛能指出其中翻译的不足...

目录:

§  原理

  § Type of Code (代码级别)

   § Execution context(执行上下文) 

   § Activation object / Variable object  

   § Property attributes(属性特性)

   § Build-ins and DontDelete(嵌入式/不可删除) 

   § Undeclared assignments (未声明任务)

§  FireBug confusion  (奇异的FireBug)

   § Deleting variables via eval(通过eval删除变量)

§  Browsers compliance (浏览器兼容性)

§  'delete' and host objects

§  ES5 strict mode 

§  Summary  

 

================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 操作符的行为

注释: 在这里我将使用JavaScriptECMAScript(这是真正意义上的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作用域.很快我们就会看到为什么这个类型是特殊的.

 

§ Execution context(执行上下文)  

  当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(属性特性)  

  我们就快要明白了, 现在我们清楚的明白变量到底发生了什么(它们属性之间的变换), 剩下 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 它还是没有被删除.

小结: 从来不要相信宿主对象

 

§ ES5 strict mode  

  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这些限制, 反向理解来学习更深入的知识. 

§ Summary  

  这篇文章说的太长了, 如果你能静下心来好好看完, 那你将会明白很多, 这里我只是说了一部分关于Array的delete我这里就不说了, 但希望有兴趣的同学可以自己去尝试(你可以参考MDC  for that particular explanation 文章).

   这是里简单的做一下在JavaScript中delete的操作:

  • 变量和函数声明属性要么是激活对象, 要么是全局对象

  • 属性里有DontDelete特性的表示不可删除属性.
  • 变量和函数声明只要是在"全局代码块"/"函数级代码块"中都会有 —— DontDelete.

  • Functions的参数也是属于激活对象的属性, 所以也有 —— DontDelete.
  • 变量和函数声明在Eval代码块中的, 都不会创建 —— DontDelete.

  • 为对象加入新的属性(没有任何特性), 也是不会创建 —— DontDelete.
  • 不管他们想怎样, 宿主对象对删除是会返回状态的.

 ================Enein翻译===================

 

 

posted @ 2017-12-03 23:33  lsgxeva  阅读(524)  评论(0编辑  收藏  举报