深入詳解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.   
  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操作的結果有可能是不可預料的。

如果你希望對以上這些有更進一步的了解,請參考ECMA規范:ECMA-262 3rd edition specification


 

原文:

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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM