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翻譯===================