[转]深入详解javascript之delete操作符

最近重新温习JS,对delete操作符一直处于一知半解的状态,偶然发现一篇文章,对此作了非常细致深入的解释,看完有茅塞顿开的感觉,不敢独享,大致翻译如下。

原文地址:http://perfectionkills.com/understanding-delete/

P.S. 作者是PrototypeJS的开发组成员之一

========分割线========

在开始之前,先让我们看一段代码

Js代码    

  1. >>> var sum = function(a, b) {return a + b;}
  2. >>> var add = sum;
  3. >>> delete sum
  4. true
  5. >>> typeof sum;
  6. "undefined"

这段代码是Firebug控制台里的实际结果,初看这段代码,你觉得有什么问题?但我要说的是,删除sum应该是失败的,同时typeof sum的结果不应该是undefined,因为在Javascript里以这种方式声明的变量是无法被删除的。

那么问题出在哪里?为了回答这个问题,我们需要理解delete操作符在各种情况下的实现细节,然后再回过头来看Firebug的这个看似“诡异”的输出。

P.S 没有特殊声明的情况下,下文中所提到的Javascript都指的是ECMAScript规范。

1. 理论

delete操作符通常用来删除对象的属性:

Js代码    

  1. var o = { x: 1 };
  2. delete o.x; // true
  3. o.x; // undefined

而不是一般的变量:

Js代码    

  1. var x = 1;
  2. delete x; // false
  3. x; // 1

或者是函数:

Js代码    

  1. function x(){}
  2. delete x; // false
  3. typeof x; // "function"

注意delete只有在无法删除的情况下才会返回false。

为了理解这一点,我们必须解释一下变量初始化以及变量属性的一些基本概念--很不幸的是很少有Javascript的书能讲到这些。如果你只想知其然而不是知其所以然的话,你完全可以跳过这一节。

代码的类型

在ECMAScript中,有三种可执行代码类型:全局代码、函数代码、eval代码。

1. 当一段代码被当做程序段运行的时候,它是在全局作用域下执行的,也就是全局代码。在浏览器环境下,通常<SCRIPT>元素就是一段全局代码。

2. 所有在function中声明的代码即是函数代码,最常见的是HTML元素的响应事件(<p onclick="...">)。

3. 传入内建的eval函数中的代码段称为eval代码,稍后我们会看到这种类型的特别性。

执行上下文(Execution Context)

在ECMAScript代码执行的时候,总是会有一个执行的上下文。这是一个比较抽象的概念,但可以帮助我们理解作用域以及变量初始化的相关过程。对于以上三种代码段类型,都有一个相应的执行上下文,比如函数代码有函数上下文,全局代码有全局上下文,等等。

逻辑上执行上下文相互间可以形成堆栈,在全局代码执行的最开始会有一个全局上下文,当调用一个函数的时候会进入相应函数的上下文,之后又可以再继续调用其他的函数亦或是递归调用自己,这时执行上下文的嵌套类似于函数调用栈。

Activation object / Variable object

每个执行上下文都和一个Variable object(变量对象)相关联 ,这也是一个抽象的概念,便于我们理解变量实例化机制:在源代码中声明的变量和方法实际上都是作为属性被加入到与当前上下文相关联的这个对象当中 。

当执行全局代码的时候,Variable object就是一个全局对象,也就是说所有全局变量和函数都是作为这个变量的属性存在。

Js代码    

  1. /* 全局环境下,this所指向的就是这个全局对象 */
  2. var GLOBAL_OBJECT = this;
  3. var foo = 1;
  4. GLOBAL_OBJECT.foo; // 1
  5. foo === GLOBAL_OBJECT.foo; // true
  6. function bar(){}
  7. typeof GLOBAL_OBJECT.bar; // "function"
  8. GLOBAL_OBJECT.bar === bar; // true

那么对于在函数中声明的变量呢?情况是类似的,函数中声明的变量也是被当做相应上下文对象的属性,唯一的区别是在函数代码段中,这个对象被称为Activation object(活动对象)。每次进入一个函数调用都会新建一个新的活动对象。

在函数段中,并不是只有显式声明的变量和函数会成为活动对象的属性,对于每个函数中隐式存在的arguments对象(函数的参数列表)也是一样的。注意活动对象其实是一种内部机制,程序代码是无法访问到的。

Js代码    

  1. (function(foo){
  2. var bar = 2;
  3. function baz(){}
  4. /*
  5. 可以吧活动对象作为一个抽象的存在,在每进入一个函数的时候,默认的arguments对象以及传入的参数都会自动被设为活动对象的属性:
  6. ACTIVATION_OBJECT.arguments; // arguments变量
  7. 传入参数foo:
  8. ACTIVATION_OBJECT.foo; // 1
  9. 函数内声明的变量bar:
  10. ACTIVATION_OBJECT.bar; // 2
  11. 以及函数内定义的baz函数:
  12. typeof ACTIVATION_OBJECT.baz; // "function"
  13. */
  14. })(1);

最后,在evel代码段中定义的变量都是被加入到当前执行eval的上下文环境对象中,也就是说进入eval代码时并不会新建新的变量对象,而是沿用当前的环境。

Js代码    

  1. var GLOBAL_OBJECT = this;
  2. /* foo被加入到当前变量对象中,也就是全局对象。 */
  3. eval(‘var foo = 1;‘);
  4. GLOBAL_OBJECT.foo; // 1
  5. (function(){
  6. /* bar被加入到当前这个函数的活动对象中。 */
  7. eval(‘var bar = 1;‘);
  8. /*
  9. 可以抽象地表示为:
  10. ACTIVATION_OBJECT.bar; // 1
  11. */
  12. })();

变量属性的标记

我 们已经知道声明变量时发生了什么(他们都变成了当前上下文对象的属性),接下来我们就要看一下属性究竟是怎么样一回事。每一个变量属性都可以有以下任意多 个属性: ReadOnly, DontEnum, DontDelete, Internal。你可以把这些当做标记,标明了变量属性可以持有的某种特性。这里我们最感兴趣的就是DontDelete标记。

在 声明变量或者函数时,他们都变成了当前上下文对象的属性--对于函数代码来说是活动对象,对于全局代码来说则是变量对象,而值得注意的是这些属性在创建时 都带有DontDelete标记,但是显式或者隐式的赋值语句所产生的属性并不会带有这个标记!这就是为什么有一些属性我们可以删除,但另一些却不可以:

Js代码    

  1. var GLOBAL_OBJECT = this;
  2. /*  foo是被正常声明的,所以带有DontDelete标记,从而不能被删除! */
  3. var foo = 1;
  4. delete foo; // false
  5. typeof foo; // "number"
  6. /* bar是作为函数被声明,一样带有DontDelete,不能被删除。 */
  7. function bar(){}
  8. delete bar; // false
  9. typeof bar; // "function"
  10. /*  baz是直接通过一个赋值而没有声明,不会持有DontDelete标记,才可以被删除! */
  11. GLOBAL_OBJECT.baz = ‘blah‘;
  12. delete GLOBAL_OBJECT.baz; // true
  13. typeof GLOBAL_OBJECT.baz; // "undefined"

内建对象与DontDelete

DontDelete就是一个特殊的标记,用来表明某一个属性能否被删除。需要注意的是一些内建的对象是自动持有这个标记的,从而不能被删除,比如函数内的arguments,以及函数的length属性。

Js代码    

  1. (function(){
  2. /*arguments对象默认持有DontDelete标记,不能被删除。 */
  3. delete arguments; // false
  4. typeof arguments; // "object"
  5. /* 函数的length属性也一样 */
  6. function f(){}
  7. delete f.length; // false
  8. typeof f.length; // "number"
  9. })();

函数的传入参数也是一样的:

Js代码    

  1. (function(foo, bar){
  2. delete foo; // false
  3. foo; // 1
  4. delete bar; // false
  5. bar; // ‘blah‘
  6. })(1, ‘blah‘);

非声明性赋值

你可能知道,非声明性的赋值语句会产生全局变量,进而变成全局变量对象的属性。所以根据上面的解释,非声明性的赋值所产生的对象是可以被删除的:

Js代码    

  1. var GLOBAL_OBJECT = this;
  2. /* 通过声明的全局变量会持有DontDelete,无法被删除。 */
  3. var foo = 1;
  4. /* 没有经过声明的变量赋值不会带DontDelete,可以被删除。 */
  5. bar = 2;
  6. delete foo; // false
  7. typeof foo; // "number"
  8. delete bar; // true
  9. typeof bar; // "undefined"

需要注意的是属性标记诸如DontDelete是在这个属性被创建的时候 产生的,之后对该属性的任何赋值都不会改变此属性的标记!

Js代码    

  1. /* foo被声明时会带有DontDelete标记 */
  2. function foo(){}
  3. /* 之后对foo的赋值无法改变他所带的标记! */
  4. foo = 1;
  5. delete foo; // false
  6. typeof foo; // "number"
  7. /* 当给一个还不存在的属性赋值的时候会创建一个不带任何标记的属性(包括DontDelete),进而可以被删除! */
  8. this.bar = 1;
  9. delete bar; // true
  10. typeof bar; // "undefined"

2. Firebug的困扰

现在再让我们回到最开始的问题,为什么在Firebug控制台里声明的变量可以被删除呢?这就要牵涉到eval代码段的特殊行为,也就是在eval中声明的变量创建时都不会带有DontDelete标记!

Js代码    

  1. eval(‘var foo = 1;‘);
  2. foo; // 1
  3. delete foo; // true
  4. typeof foo; // "undefined"

在函数内部也是一样的:

Js代码    

  1. (function(){
  2. eval(‘var foo = 1;‘);
  3. foo; // 1
  4. delete foo; // true
  5. typeof foo; // "undefined"
  6. })();

这就是导致Firebug"诡异"行为的罪魁祸首: 在Firebug控制台中的代码最终将通过eval执行,而不是作为全局代码或函数代码。显然地,这样声明出来的变量都不会带DontDelete标记,所以才能被删除!(译者:也不能太信任Firebug啊。)

3. Browsers Compliance

//译者:这一节讲了主流浏览器对一些delete的特殊情况的不同处理,篇幅所限暂不赘述,有兴趣的可以参看原文。

4. IE bugs

是的,你没有看错,整个这一节都是在讲IE的bug!

在IE6-8中,下面的代码会抛出错误(全局代码):

Js代码    

  1. this.x = 1;
  2. delete x; // TypeError: Object doesn‘t support this action
  3. var x = 1;
  4. delete this.x; // TypeError: Cannot delete ‘this.x‘

看起来似乎在IE里变量声明并不会在全局变量对象里产生相应的属性。还有更有趣的,对于显式赋值的属性总是会在删除时出错,并不是真正抛出错误,而是这些属性似乎都带有DontDelete标记,和我们的设想相反。

Js代码    

  1. this.x = 1;
  2. delete this.x; // TypeError: Object doesn‘t support this action
  3. typeof x; // "number" (没有被删除!)
  4. delete x; // TypeError: Object doesn‘t support this action
  5. typeof x; // "number" (还是没有被删除!)

但下面的代码表明,非声明性的赋值产生的属性确实是可以删除的:

Js代码    

  1. x = 1;
  2. delete x; // true
  3. typeof x; // "undefined"

不过当你试图通过全局变量对象this来访问x的时候,错误又来了:

Js代码    

  1. x = 1;
  2. delete this.x; // TypeError: Cannot delete ‘this.x‘

总 而言之,通过全局this变量去删除属性(delete this.x)总会出错,而直接删除该属性(delete x)时:如果x是通过全局this赋值产生会(this.x=1)导致错误;如果x通过显式声明创建(var x=1)则delete会像我们预料的那样无法删除并返回false;如果x通过非声明式赋值创建(x=1)则delete可以正常删除。

对于以上的问题,Garrett Smith 的一个解释是"IE的全局变量对象是通过JScript实现,而一般的全局变量是由host实现的。"(ref: Eric Lippert’s blog entry )

我们可以自己验证一下这个解释,注意this和window看上去是指向同一个对象,但是函数所返回的当前环境的变量对象却和this不同。

Js代码    

  1. /* in Global code */
  2. function getBase(){ return this; }
  3. getBase() === this.getBase(); // false
  4. this.getBase() === this.getBase(); // true
  5. window.getBase() === this.getBase(); // true
  6. window.getBase() === getBase(); // false

5. 错误的理解

//译者: 大意为网上对Javascript一些行为有各种不同的解释,有的甚至可能完全矛盾,不要轻易相信别人的解释,试着自己去寻找问题的核心:)

6. delete与宿主对象(host objects)

delete的大致算法如下:

1. 如果操作对象不是一个引用,返回true

2. 如果当前上下文对象没有此名字的一个直接属性,返回true(上下文对象可以是全局对象或者函数内的活动对象)

3. 如果存在这样一个属性但是有DontDelete标记,返回false

4. 其他情况则删除该属性并返回true

然而有一个例外,即对于宿主对象而言,delete操作的结果是不可预料的。这并不奇怪,因为宿主对象根据不同浏览器的实现允许有不同的行为,这其中包括了delete。所以当处理宿主对象时,其结果是不可信的,比如在FF下:

Js代码    

  1. /* "alert" 是window对象的一个属性 */
  2. window.hasOwnProperty(‘alert‘); // true
  3. delete window.alert; // true
  4. typeof window.alert; // "function",表明实际上并没有真正删除

总而言之,任何时候都不要相信宿主对象。

7. ES5 strict mode

为了能更早地发现一些应该被发现的问题,ECMAScript 5th edition 提出了strict mode的概念。下面是一个例子:

Js代码    

  1. (function(foo){
  2. "use strict"; // enable strict mode within this function
  3. var bar;
  4. function baz(){}
  5. delete foo; // SyntaxError (when deleting argument)
  6. delete bar; // SyntaxError (when deleting variable)
  7. delete baz; // SyntaxError (when deleting variable created with function declaration)
  8. /* `length` of function instances has { [[Configurable]] : false } */
  9. delete (function(){}).length; // TypeError
  10. })();

删除不存在的变量:

Js代码    

  1. "use strict";
  2. delete i_dont_exist; // SyntaxError

对未声明的变量赋值:

Js代码    

  1. "use strict";
  2. i_dont_exist = 1; // ReferenceError

可以看出,strict mode采用了更主动并且描述性的方法,而不是简单的忽略无效的删除操作。

8. 总结

  • 变量和函数的声明实际上都会成为全局对象或者当前函数活动对象的属性。
  • 属性都有一个DontDelete标记,用于表明该属性是否能被delete。
  • 变量和函数的声明创建的属性都会带有DontDelete标记。
  • 函数内建的arguments对象作为该函数活动对象的默认属性,创建时总会带有DontDelete标记。
  • 在eval代码块中声明的变量和方法都不带有DontDelete标记。
  • 对还不存在的变量或属性的直接赋值产生的对象不会带有任何标记,包括DontDelete。
  • 对于宿主对象而言,delete操作的结果有可能是不可预料的。

如果你希望对以上这些有更进一步的了解,请参考ECMA规范:ECMA-262 3rd edition specification

原文:

http://m.oschina.net/blog/28926

时间: 2024-10-07 22:08:08

[转]深入详解javascript之delete操作符的相关文章

详解javascript中的this对象

详解javascript中的this对象 前言 Javascript是一门基于对象的动态语言,也就是说,所有东西都是对象,一个很典型的例子就是函数也被视为普通的对象.Javascript可以通过一定的设计模式来实现面向对象的编程,其中this "指针"就是实现面向对象的一个很重要的特性.但是this也是Javascript中一个非常容易理解错,进而用错的特性.特别是对于接触静态语言比较久了的同志来说更是如此. 示例说明 我们先来看一个最简单的示例: <script type=&q

详解javascript闭包特性

闭包(closure)是Javascript语言的一个难点,也是它的特色,很多高级应用都要依靠闭包实现. 闭包有三个特性: 1.函数嵌套函数 2.函数内部可以引用外部的参数和变量 3.参数和变量不会被垃圾回收机制回收 闭包是指有权访问另一个函数作用域中的变量的函数,创建闭包的最常见的方式就是在一个函数内创建另一个函数,通过另一个函数访问这个函数的局部变量 使用闭包有一个优点,也是它的缺点,就是可以把局部变量驻留在内存中,可以避免使用全局变量.全局变量在每个模块都可调用,这势必将是灾难性的. 所以

091:QuerySet API详解-update和delete

QuerySet API详解-update和delete: update :执行更新操作,在 SQL 底层走的也是 update 命令.比如要将所有 图书的价格提高五元.示例代码如下: Book.objects.update(price=F("price") + 5) # 对比一下上下两种方式 # books = Book.objects.all() # for item in books: # item.price += 5 # item.save() 注意这个方法走的是更新的逻辑.

详解JavaScript调用栈、尾递归和手动优化

调用栈(Call Stack) 调用栈(Call Stack)是一个基本的计算机概念,这里引入一个概念:栈帧. 栈帧是指为一个函数调用单独分配的那部分栈空间. 当运行的程序从当前函数调用另外一个函数时,就会为下一个函数建立一个新的栈帧,并且进入这个栈帧,这个栈帧称为当前帧.而原来的函数也有一个对应的栈帧,被称为调用帧.每一个栈帧里面都会存入当前函数的局部变量. 当函数被调用时,就会被加入到调用栈顶部,执行结束之后,就会从调用栈顶部移除该函数.并将程序运行权利(帧指针)交给此时栈顶的栈帧.这种后进

详解Javascript中的Object对象

Object是在javascript中一个被我们经常使用的类型,而且JS中的所有对象都是继承自Object对象的.虽说我们平时只是简单地使用了Object对象来存储数据,并没有使用到太多其他功能,但是Object对象其实包含了很多很有用的属性和方法,尤其是ES5增加的方法,因此,本文将从最基本的介绍开始,详细说明了Object的常用方法和应用. 基础介绍 创建对象 首先我们都知道,对象就是一组相似数据和功能的集合,我们就是用它来模拟我们现实世界中的对象的.那在Javascript中,创建对象的方

详解JavaScript中的arc的方法

今天说说JavaScript在网页中画圆的函数arc! 一.arc所需要的参数设置 1 arc(x, y, radius, startAngle, endAngle, counterclockwise); 其中x,y,radius都很容易理解,那么重点说说startAngle,endAngle和counterclockwise三个参数!    二.arc参数详解 1,startAngle和endAngle分别指圆开始的角度和结束的角度,手册上面说的是开始的角度为0,结束的角度为Math.PI*2

【转】详解JavaScript中的异常处理方法

有三种类型的编程错误:(1)语法错误和(2)运行时错误(3)逻辑错误:语法错误: 语法错误,也被称为解析错误,在编译时进行传统的编程语言,并出现在JavaScript解释时. 例如,下面一行将导致一个语法错误,因为它缺少一个右括号: <script type="text/javascript"> <!-- window.print(; //--> </script> 当一个语法错误在JavaScript中出现,只有在同一个线程中包含的语法错误的影响,

详解JavaScript中的replace()函数

Javascript中字符串对象有一个方法replace(),它的作用非常强大.这里把它的用法整理一下.  一.方法简介 该方法的签名是:replace([RegExp|String],[String|Function]). 该方法 返回一个新的字符串,但并不改变字符串本身. 该方法接收2个参数,第一个参数可以是字符串,也可以是一个正则表达式:第二个参数可以是一个字符串,也可以是一个函数.其中第2个参数如果是函数,那么用起来是十分强大而且灵活的,不过相对来说也比较难掌握.下面就其用法进行详细说明

详解JavaScript中的Url编码/解码,表单提交中网址编码

本文主要针对URI编解码的相关问题做了介绍,对Url编码中哪些字符需要编码.为什么需要编码做了详细的说明,并对比分析了Javascript 中和 编解码相关的几对函数escape / unescape,encodeURI / decodeURI和 encodeURIComponent / decodeURIComponent. 预备知识 foo://example.com:8042/over/there?name=ferret#nose \_/ \______________/ \_______