js delete

在開始以前,先讓咱們看一段代碼javascript

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裏以這種方式聲明的變量是沒法被刪除的。html

那麼問題出在哪裏?爲了回答這個問題,咱們須要理解delete操做符在各類狀況下的實現細節,而後再回過頭來看Firebug的這個看似「詭異」的輸出。java

P.S 沒有特殊聲明的狀況下,下文中所提到的Javascript都指的是ECMAScript規範。算法

 

1. 理論瀏覽器

 delete操做符一般用來刪除對象的屬性:app

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。ui

爲了理解這一點,咱們必須解釋一下變量初始化以及變量屬性的一些基本概念--很不幸的是不多有Javascript的書能講到這些。若是你只想知其然而不是知其因此然的話,你徹底能夠跳過這一節。this

 

代碼的類型

在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.   
  4. var foo = 1;  
  5. GLOBAL_OBJECT.foo; // 1  
  6. foo === GLOBAL_OBJECT.foo; // true  
  7.   
  8. function bar(){}  
  9. typeof GLOBAL_OBJECT.bar; // "function"  
  10. GLOBAL_OBJECT.bar === bar; // true  

 

那麼對於在函數中聲明的變量呢?狀況是相似的,函數中聲明的變量也是被當作相應上下文對象的屬性,惟一的區別是在函數代碼段中,這個對象被稱爲Activation object(活動對象)。每次進入一個函數調用都會新建一個新的活動對象。

 

在函數段中,並非只有顯式聲明的變量和函數會成爲活動對象的屬性,對於每一個函數中隱式存在的arguments對象(函數的參數列表)也是同樣的。注意活動對象實際上是一種內部機制,程序代碼是沒法訪問到的。

Js代碼      收藏代碼
  1. (function(foo){  
  2.   
  3.   var bar = 2;  
  4.   function baz(){}  
  5.   
  6.   /* 
  7.   能夠吧活動對象做爲一個抽象的存在,在每進入一個函數的時候,默認的arguments對象以及傳入的參數都會自動被設爲活動對象的屬性:  
  8.     ACTIVATION_OBJECT.arguments; // arguments變量 
  9.  
  10.   傳入參數foo: 
  11.     ACTIVATION_OBJECT.foo; // 1 
  12.  
  13.     函數內聲明的變量bar: 
  14.     ACTIVATION_OBJECT.bar; // 2 
  15.  
  16.     以及函數內定義的baz函數: 
  17.     typeof ACTIVATION_OBJECT.baz; // "function" 
  18.   */  
  19.   
  20. })(1);  

 最後,在evel代碼段中定義的變量都是被加入到當前執行eval的上下文環境對象中,也就是說進入eval代碼時並不會新建新的變量對象,而是沿用當前的環境。

 

Js代碼      收藏代碼
  1. var GLOBAL_OBJECT = this;  
  2.   
  3. /* foo被加入到當前變量對象中,也就是全局對象。 */  
  4.   
  5. eval('var foo = 1;');  
  6. GLOBAL_OBJECT.foo; // 1  
  7.   
  8. (function(){  
  9.   
  10.   /* bar被加入到當前這個函數的活動對象中。 */  
  11.   
  12.   eval('var bar = 1;');  
  13.   
  14.   /*  
  15.     能夠抽象地表示爲:  
  16.     ACTIVATION_OBJECT.bar; // 1 
  17.   */  
  18.   
  19. })();  

 

變量屬性的標記

咱們已經知道聲明變量時發生了什麼(他們都變成了當前上下文對象的屬性),接下來咱們就要看一下屬性到底是怎麼樣一回事。每個變量屬性均可以有如下任意多個屬性: ReadOnly, DontEnum, DontDelete, Internal。你能夠把這些當作標記,標明瞭變量屬性能夠持有的某種特性。這裏咱們最感興趣的就是DontDelete標記。

 

在聲明變量或者函數時,他們都變成了當前上下文對象的屬性--對於函數代碼來講是活動對象,對於全局代碼來講則是變量對象,而值得注意的是這些屬性在建立時都帶有DontDelete標記,可是顯式或者隱式的賦值語句所產生的屬性並不會帶有這個標記!這就是爲何有一些屬性咱們能夠刪除,但另外一些卻不能夠:

Js代碼      收藏代碼
  1. var GLOBAL_OBJECT = this;  
  2.   
  3. /*  foo是被正常聲明的,因此帶有DontDelete標記,從而不能被刪除! */  
  4.   
  5. var foo = 1;  
  6. delete foo; // false  
  7. typeof foo; // "number"  
  8.   
  9. /* bar是做爲函數被聲明,同樣帶有DontDelete,不能被刪除。 */  
  10.   
  11. function bar(){}  
  12. delete bar; // false  
  13. typeof bar; // "function"  
  14.   
  15. /*  baz是直接經過一個賦值而沒有聲明,不會持有DontDelete標記,才能夠被刪除! */  
  16.   
  17. GLOBAL_OBJECT.baz = 'blah';  
  18. delete GLOBAL_OBJECT.baz; // true  
  19. typeof GLOBAL_OBJECT.baz; // "undefined"  

 

內建對象與DontDelete

DontDelete就是一個特殊的標記,用來代表某一個屬性可否被刪除。須要注意的是一些內建的對象是自動持有這個標記的,從而不能被刪除,好比函數內的arguments,以及函數的length屬性。

Js代碼      收藏代碼
  1. (function(){  
  2.   
  3.   /*arguments對象默認持有DontDelete標記,不能被刪除。 */  
  4.   
  5.   delete arguments; // false  
  6.   typeof arguments; // "object"  
  7.   
  8.   /* 函數的length屬性也同樣 */  
  9.   
  10.   function f(){}  
  11.   delete f.length; // false  
  12.   typeof f.length; // "number"  
  13.   
  14. })();  

 函數的傳入參數也是同樣的:

Js代碼      收藏代碼
  1. (function(foo, bar){  
  2.   
  3.   delete foo; // false  
  4.   foo; // 1  
  5.   
  6.   delete bar; // false  
  7.   bar; // 'blah'  
  8.   
  9. })(1, 'blah');  

 

 非聲明性賦值

你可能知道,非聲明性的賦值語句會產生全局變量,進而變成全局變量對象的屬性。因此根據上面的解釋,非聲明性的賦值所產生的對象是能夠被刪除的:

Js代碼      收藏代碼
  1. var GLOBAL_OBJECT = this;  
  2.   
  3. /* 經過聲明的全局變量會持有DontDelete,沒法被刪除。 */  
  4. var foo = 1;  
  5.   
  6. /* 沒有通過聲明的變量賦值不會帶DontDelete,能夠被刪除。 */  
  7. bar = 2;  
  8.   
  9. delete foo; // false  
  10. typeof foo; // "number"  
  11.   
  12. delete bar; // true  
  13. typeof bar; // "undefined"  

 

須要注意的是屬性標記諸如DontDelete是在這個屬性被建立的時候 產生的,以後對該屬性的任何賦值都不會改變此屬性的標記!

Js代碼      收藏代碼
  1. /* foo被聲明時會帶有DontDelete標記 */  
  2. function foo(){}  
  3.   
  4. /* 以後對foo的賦值沒法改變他所帶的標記! */  
  5. foo = 1;  
  6. delete foo; // false  
  7. typeof foo; // "number"  
  8.   
  9. /* 當給一個還不存在的屬性賦值的時候會建立一個不帶任何標記的屬性(包括DontDelete),進而能夠被刪除! */  
  10.   
  11. this.bar = 1;  
  12. delete bar; // true  
  13. 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.   
  3.   eval('var foo = 1;');  
  4.   foo; // 1  
  5.   delete foo; // true  
  6.   typeof foo; // "undefined"  
  7.   
  8. })();  

 這就是致使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.   
  4. var x = 1;  
  5. delete this.x; // TypeError: Cannot delete 'this.x'  

 看起來彷佛在IE裏變量聲明並不會在全局變量對象裏產生相應的屬性。還有更有趣的,對於顯式賦值的屬性老是會在刪除時出錯,並非真正拋出錯誤,而是這些屬性彷佛都帶有DontDelete標記,和咱們的設想相反。

Js代碼      收藏代碼
  1. this.x = 1;  
  2.   
  3. delete this.x; // TypeError: Object doesn't support this action  
  4. typeof x; // "number" (沒有被刪除!)  
  5.   
  6. delete x; // TypeError: Object doesn't support this action  
  7. 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.   
  4. getBase() === this.getBase(); // false  
  5. this.getBase() === this.getBase(); // true  
  6. window.getBase() === this.getBase(); // true  
  7. 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.   
  4. delete window.alert; // true  
  5. typeof window.alert; // "function",代表實際上並無真正刪除  

 總而言之,任什麼時候候都不要相信宿主對象。

 

7. ES5 strict mode

爲了能更早地發現一些應該被發現的問題,ECMAScript 5th edition 提出了strict mode的概念。下面是一個例子:

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

 刪除不存在的變量:

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操做的結果有多是不可預料的。
相關文章
相關標籤/搜索