在javascript中,我們有時候要使用delete刪除對象。但是,對於delete的一些細節我們未必盡知。昨天,看到kangax分析delete的文章,獲益匪淺。本文將文章的精華部分翻譯出來,與各位分享。
原理
為什麼我們能刪除一個對象的屬性?
var o = { x: 1 }; delete o.x; // true o.x; // undefined
但是,像這樣聲明的變量則不行:
var x = 1; delete x; // false x; // 1
或者如此聲明的函數:
function x(){} delete x; // false typeof x; // "function"
注意,當一個屬性不能被刪除時,delete只返回false。
要理解這一點,我們首先需要掌握像變量實例化和屬性特性這樣的概念--遺憾的是這些在關於javascript的書中很少講到。我將在接下來的幾個段落中試著簡明的重溫這些概念。 理解它們一點也不難,如果你不在乎它們為什麼這麼運行,你可以隨意的跳過這一章。
代碼類型
在ECMAScript中有三種類型的可執行代碼:全局代碼(Global code)、函數代碼(Function code)和Eval code。這些類型有那麼點自我描述,但這裡還是作一個簡短的概述:
<p onclick="...">)通常被當作函數代碼(Function code)來解析;
執行上下文
當ECMAScript 代碼執行時,它總是在一定的上下文中運行,執行上下文是一個有點抽象的實體,它有助於我們理解作用域和變量實例化如何工作的。對於三種類型的可執行代碼,每個都有執行的上下文。當一個函數執行時,可以說控制進入到函數代碼(Function code)的執行上下文。全局代碼執行時,進入到全局代碼(Global code)的執行上下文。
正如你所見,執行上下文邏輯上來自一個棧。首先可能是有自己作用域的全局代碼,代碼中可能調用一個函數,它有自己的作用域,函數可以調用另外一個函數,等等。即使函數遞歸地調用它自身,每一次調用都進入一個新的執行上下文。
激活對象/可變對象
每一個執行上下文在其內部都有一個所謂的可變對象。與執行上下文類似,可變對象是一個抽象的實體,一個描述變量示例化的機制。現在,最有趣的是在源代碼中聲明的變量和函數被當作這個可變對象的屬性被添加。
當控制進入全局代碼的執行上下文時,一個全局對象用作可變對象。這也正是為什麼在全局范圍中聲明的變量或者函數變成了全局對象的屬性。
/* 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,全局變量變成了全局對象的屬性,但是,那些在函數代碼(Function code)中定義的局部變量又會如何呢?行為其實很相似:它成了可變對象的屬性。唯一的差別在於在函數代碼(Function code)中,可變對象不是全局對象,而是所謂的激活對象。每次函數代碼(Function code)進入執行作用域時,激活對象即被創建。
不僅函數代碼(Function code)中的變量和函數成為激活對象的屬性,而且函數的每一個參數(與形參相對應的名稱)和一個特定Arguments 對象(Arguments )也是。注意,激活對象是一種內部機制,不會被程序代碼真正訪問到。
(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 代碼(Eval code)中聲明的變量作為正在調用的上下文的可變對象的屬性被創建。Eval 代碼(Eval code)只使用它正在被調用的哪個執行上下文的可變對象。
var GLOBAL_OBJECT = this; /* `foo` is created as a property of calling context Variable object, which in this case is a Global object */eval('var foo = 1;'); GLOBAL_OBJECT.foo; // 1 (function(){ /* `bar` is created as a property of calling context Variable object, which in this case is an Activation object of containing function */ eval('var bar = 1;'); /* In abstract terms, ACTIVATION_OBJECT.bar; // 1 */})();
屬性特性
現在變量會怎樣已經很清楚(它們成為屬性),剩下唯一的需要理解的概念是屬性特性。每個屬性都有來自下列一組屬性中的零個或多個特性--ReadOnly, DontEnum, DontDelete 和Internal,你可以認為它們是一個標記,一個屬性可有可無的特性。為了今天討論的目的,我們只關心DontDelete 特性。
當聲明的變量和函數成為一個可變對象的屬性時--要麼是激活對象(Function code),要麼是全局對象(Global code),這些創建的屬性帶有DontDelete 特性。但是,任何明確的(或隱含的)創建的屬性不具有DontDelete 特性。這就是我們為什麼一些屬性能刪除,一些不能。
var GLOBAL_OBJECT = this; /* `foo` is a property of a Global object. It is created via variable declaration and so has DontDelete attribute. This is why it can not be deleted. */ var foo = 1; delete foo; // false typeof foo; // "number" /* `bar` is a property of a Global object. It is created via function declaration and so has DontDelete attribute. This is why it can not be deleted either. */ function bar(){} delete bar; // false typeof bar; // "function" /* `baz` is also a property of a Global object. However, it is created via property assignment and so has no DontDelete attribute. This is why it can be deleted. */ GLOBAL_OBJECT.baz = 'blah'; delete GLOBAL_OBJECT.baz; // true typeof GLOBAL_OBJECT.baz; // "undefined"
內置對象和DontDelete
這就是全部:屬性中一個獨特的特性控制著這個屬性是否能被刪除。注意,內置對象的一些屬性也有特定的DontDelete 特性,因此,它不能被刪除。特定的Arguments 變量(或者,正如我們現在了解的,激活對象的屬性),任何函數實例的length屬性也擁有DontDelete 特性。
function(){ /* can't delete `arguments`, since it has DontDelete */ delete arguments; // false typeof arguments; // "object" /* can't delete function's `length`; it also has DontDelete */ 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');
未聲明的賦值
您可能還記得,未聲明的賦值在一個全局對象上創建一個屬性。除非它在全局對象之前的作用域中的某個地方可見。現在我們知道屬性分配與變量聲明之間的差異,後者設置了DontDelete 特性,而前者沒有--應該很清楚未聲明的賦值創建了一個可刪除的屬性。
var GLOBAL_OBJECT = this; /* create global property via variable declaration; property has <STRONG>DontDelete</STRONG> */var foo = 1; /* create global property via undeclared assignment; property has no <STRONG>DontDelete</STRONG> */bar = 2; delete foo; // false typeof foo; // "number" delete bar; // true typeof bar; // "undefined"
請注意,該特性是在屬性創建的過程中確定的(例如:none)。後來的賦值不會修改現有屬性已經存在的特性,理解這一點很重要。
/* `foo` is created as a property with DontDelete */function foo(){} /* Later assignments do not modify attributes. DontDelete is still there! */foo = 1; delete foo; // false typeof foo; // "number" /* But assigning to a property that doesn't exist, creates that property with empty attributes (and so without DontDelete) */this.bar = 1; delete bar; // true typeof bar; // "undefined"
Firebug 困惑
那麼,在Firebug中會發生什麼呢?為什麼在控制台中定義的變量可以被刪除,難道與我們剛才了解到相反?很好,我先前說過,當涉及到的變量聲明,Eval 代碼(Eval code)有一個特殊的行為。在Eval 代碼(Eval code)中聲明的變量實際上沒有創建DontDelete 特性。
eval('var foo = 1;'); foo; // 1 delete foo; // true typeof foo; // "undefined"confusion
同樣,在函數代碼(Function code)調用也是如此:
(function(){ eval('var foo = 1;'); foo; // 1 delete foo; // true typeof foo; // "undefined" })();
這是Firebug的異常行為的要點,在控制台的所有文本似乎是作為Eval 代碼(Eval code)來解析和執行的,而不是作為一個全局對象或函數對象,顯然,任何聲明的變量沒有DontDelete特性,因此可以很容易地刪除,應該意識到正常全局代碼和Firebug控制台之間的分歧。
通過eval刪除變量
這個有趣的eval屬性,連同ECMAScript 其它方面的技巧可以讓我們刪除不可刪除的屬性。在同一個執行上下文中,函數聲明能覆蓋同一名字的變量。
function x(){ } var x; typeof x; // "function"
注意函數如何獲得優先權並覆蓋同名變量(或者換句話說,可變對象相同的屬性)。這是因為函數聲明在變量聲明之後實例化,並且可以覆蓋它們。函數聲明不僅取代了先前的屬性值,而且也取代了屬性特性。如果我們通過eval聲明函數,該函數也應該替換自身的屬性特性。既然在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"
遺憾的是,這類欺騙在我嘗試中並不總是運行,我可能丟失了一些東西,或者這種行為過於簡單不足以引起注意。
浏覽器兼容性
從理論上認識事物的工作原理是有用的,但實際影響是至關重要的。當涉及到variable/property creation/deletion時,浏覽器遵循標准嗎?在大多數是的。
我寫了一個簡單的測試包檢測Global code、Function code 和Eval code代碼delete 運算符的兼容性。測試包同時檢查 -- delete運算符的返回值,以及應被刪除的屬性是否被刪除的。delete 運算符返回true或false並不重要,重要的是有DontDelete特性不被刪除,反之亦然。
現代浏覽器一般都相當兼容,除了這個我早期提到的這個eval特性。下面的浏覽器完全通過測試包:Opera 7.54+、Firefox 1.0+、Safari 3.1.2+、Chrome 4+。
Safari 2.x 和3.0.4在刪除函數參數時有些問題,這些屬性似乎沒有創建DontDelete,所以可以刪除它們。Safari 2.x 甚至有更多問題,刪除非引用(例如delete 1)拋出錯誤;函數聲明創建了可刪除屬性(但奇怪是變量聲明不是),在eval中的變量聲明成為不可刪除的(但函數聲明不是)。
與Safari相似,Konqueror (3.5,但不是 4.3)當刪除非引用(例如delete 1)拋出錯誤,它錯誤使函數參數可以刪除。
Gecko DontDelete bug
Gecko 1.8.x浏覽器--Firefox 2.x、 Camino 1.x、Seamonkey 1.x等顯示一個有趣的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)
出乎意料的是,IE5.5 – 8全部通過測試包,刪除非引用(例如delete 1)拋出錯誤(就像在老版的Safari一樣)。但事實上有更嚴重bug存在IE中,這不會立即顯現。這些bug都與全局對象相關。
IE bugs
整個章節僅僅為了IE中的bug,想不到吧!
在IE浏覽器中(至少是IE6-IE8),下面的表達式拋出錯誤(在全局代碼中執行):
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刪除將拋出另外一個錯誤。
但這還沒完。實際上通過明確的賦值創建的屬性在刪除時始終引發錯誤。這不僅是一個錯誤,而且創建的屬性似乎設置了DontDelete特性,這當然不應該有:
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"
但是,如果您嘗試通過“this”引用在全局代碼中刪除它(delete this.x ),一個熟悉的錯誤彈出:
x = 1; delete this.x; // TypeError: Cannot delete 'this.x'
如果我們總結這些行為,從全局代碼中delete this.x 似乎是不成功的。當涉及到的屬性是通過顯式聲明(this.x = 1 )來創建的,delete 將拋出一個錯誤。當屬性是通過未聲明的賦值(x = 1 )或聲明(var x = 1 )來創建屬性時,delete 將拋出另一個錯誤。
另一方面,當涉及到的屬性是通過顯式聲明(this.x = 1 )創建時,delete x 拋出錯誤。如果一個屬性是通過聲明(var x = 1 )來創建的,刪除根本不會發生,並返回正確的false。如果屬性是通過未聲明的方式(x = 1)創建,刪除操作將按預期進行。
去年九月我正在思考這個問題,Garrett Smith 建議“在IE中全局可變對象作為一個JScript對象,全局對象有宿主執行”。Garrett 引用Eric Lippert’s blog entry ,我們可以通過一些測試驗證這些理論。請注意,this和window似乎引用同一對象(如果我們相信“===”運算符),但可變對象(在一個聲明的函數中的對象)不同於這一點。
/* in Global code */function getBase(){ return this; } getBase() === this.getBase(); // false this.getBase() === this.getBase(); // true window.getBase() === this.getBase(); // true 7.window.getBase() === getBase(); // false
誤區
理解事物為什麼那麼工作是一種難以言說的美,我在網上已經看到了與delete運算符誤解相關的誤區。例如,在關於棧溢出的回答(評分出其不意的效果高)中,它自信的解釋道:“delete is supposed to be no-op when target isn’t an object property ”。現在,我們已經理解了delete 行為的核心,很清楚這個答案是不准確的。delete 不區分變量和屬性(事實上,對於刪除,這些都是引用),真正的只關心的是DontDelete特性(和屬性存在)。
非常有意思的看到這個誤解如何相互影響,在同樣一個線程中,有人首先提出要直接刪除變量(除非它是在eval中聲明,否則不會生效),接著另外一個人提出一種錯誤的糾正方法--在全局中可能刪除變量,但在函數內不行。
在網站上解釋Javascript 最好小心,最好總是抓住問題的核心。
‘delete’和宿主對象
delete 的算法大概是這樣:
true; true;(我們知道,對象可以是激活對象,可以是全局象); false; true; 但是,宿主對象的delete 運算符的行為難以預測。實際上並沒有錯:除了少數幾個,宿主對象是允許執行任何類型的運算行為的(按規范),如read(內部的[get]方法)、write(內部的[put]方法)或delete(內部的[delete]方法)。這個定制的[[Delete]]行為使得宿主對象如此混亂。
在IE中我們已經看到一些古怪的行為,如果刪除某些對象(明顯作為宿主對象來執行)將拋出錯誤。Firefox的一些版本在嘗試刪除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,雖然這個屬性什麼也沒有,它應該導致這個結果。它保留了一個引用(因此在第一步中不應該返回true),它是窗口對象的直接屬性(因此第二步中不能返回true)。唯一的辦法讓delete返回true是在第四步之後真正刪除屬性。但是,屬性是永遠不會被刪除的。
這個故事的寓意在於永遠不要相信宿主對象
ES5嚴格模式
那麼,ECMAScript 5th edition 的嚴格模式可以拿到台面上來了。一些限制正被引入,當delete運算符是一個變量、函數參數或函數標識符的直接引用時將拋出SyntaxError。另外,如果屬性內部有[[Configurable]] == false,將拋出TypeError。
(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 })();
另外,刪除未聲明的變量(換句話說,沒有找到的引用)也拋出SyntaxError。
"use strict"; delete i_dont_exist; // SyntaxError
正如你所理解的那樣,考慮到刪除變量、函數聲明和參數會導致如此多得混淆,所有這些限制就有點意義。與不聲不響的忽略刪除行為相反,嚴格模式應該采取更積極的、更具有描述性的措施。
總結
這篇文章是冗長的,我打算去討論用delete刪除數組選項和它的含義。你可以隨時參考MDC 的文章了解具體的解釋(或閱讀規范,自己實驗)。
這是Javascript中delete運算符工作的簡短概要:
如果你想了解更多這裡這裡描述的東西,請參閱ECMA-262 3rd edition specification。
我希望你喜歡這篇綜述,並能學到新東西。任何疑問、建議、更正,一律歡迎。
相關閱讀:
原文地址:Understanding delete