原文:Understanding deletejavascript
譯文:javascript 中的 deletehtml
譯者:@justjavacjava
在這篇文章中做者從《JavaScript面向對象編程指南》一書中關於 delete 的錯誤講起,詳細講述了關於 delete 操做的實現, 侷限以及在不一樣瀏覽器和插件(這裏指 firebug)中的表現。git
下面翻譯其中的主要部分。github
...書中聲稱算法
「函數就像一個普通的變量那樣——能夠拷貝到不一樣變量,甚至被刪除」chrome
並附上了下面的代碼片斷做爲說明:編程
>>> var sum = function(a, b) {return a+b;}; >>> var add = sum; >>> delete sum; true >>> typeof sum; "undefined"
你能發現片斷中的問題嗎? 這個問題就是——刪除 sum 變量的操做不該該成功; delete 的聲明不該該返回 true 而 typeof sum 也不該該返回爲 undefined。 由於,javascript 中不可以刪除變量,至少不能以這個方式聲明刪除。數組
那麼這個例子發生了什麼? 是打印錯誤或者玩笑? 應該不是。 這個片斷是 firebug 控制檯中的一個實際輸出,而 Stoyan(上面所說書的做者)應該正是用它作的快速測試。 這彷彿說明了 firebug 有一些不一樣的刪除規則。 正是 firebug 誤導了 Stoyan! 那麼這裏面到底是怎麼回事呢?瀏覽器
爲了回答這個問題,咱們須要瞭解 delete 運算符在 Javascript 中是如何工做的: 哪些能夠被刪除,哪些不能刪除以及爲何。 下面我試着解釋一下這方面的細節。 咱們將經過觀察 firebug 的「奇怪」的表現而認識到它實際上徹底不「奇怪」; 咱們將深刻了解那些,當咱們聲明變量、函數,賦值屬性和刪除它們時的,隱藏在背後的細節; 咱們將看一下瀏覽器對此的實現和一些有名的 bug; 咱們還會討論到 ECMAScript 版本 5 中的嚴格模式(strict mode)以及它如何改變 delete 運算符的行爲。
我在下面交替使用的 Javascript 和 ECMPScript 通常都指 ECMAScript(除非當明確談到 Mozilla 的 JavaScript™ 實現時)。
意料之中的,網絡上目前對於 delete 的解釋很是少(筆者按:這篇文章寫於 2010 年 1 月)。 MDC(MDN]) 的資源大概是這其中最詳細的了,但不幸的是它遺漏了一些有趣的細節,這些細節中就包括了上述 firebug 的奇怪表現。 MSDN 文檔幾乎沒什麼用處。
那麼,爲何咱們能刪除一個對象的屬性:
var x = { a: 1 }; delete x.a; // true x.a; // undefined
但卻不能刪除一個變量:
var x = 1; delete x; // false; x; // 1
也不能刪除一個函數:
function x() {}; delete x; // false; typeof x; // "function"
注意:delete 只有當一個屬性沒法被刪除時才返回 false。
爲了理解這一點,咱們須要首先把握一些概念: 變量實例化(variable instantiation)和屬性的內部屬性(property attributes) (譯者按:關於 property 和 attributes 的區別見參考文章,根據下面涉及到的內容,擬譯成內部屬性) ——這些不多在 javascript 書中被提到。 在下面幾段中我將試着簡短地回顧這些內容,要理解它們並不難。 若是你並不關注它們表現背後的緣由,能夠跳過這一章。
ECMAScript 中有三類可執行代碼:
這幾類的含義大體就像它們命名的那樣,但仍是快速地回顧一下:
當一個源文件被看作是一個程序,它在全局做用域(scope)內執行,而這就被認爲是一段全局代碼 Global code。 在瀏覽器環境下,SCRIPT 元素的內容一般都被解析爲一個程序,於是做爲全局代碼來執行。
固然,任何在一段函數中直接執行的代碼就被認爲是一段函數代碼 Function code, 在瀏覽器環境下,事件屬性的內容(e.g. <a onclick="..."
)一般都做爲函數代碼來解析和執行。
最後,放入內建函數 eval
中的代碼就做爲 Eval code 來解析。咱們將很快看到爲何這一類型是特殊的。
當 ECMAScript 代碼執行時,它老是發生在一個肯定的執行上下文(context)中。 執行做用域是一個抽象實體,它有助於理解做用域和變量實例化的工做原理。 上面三類可執行代碼都有各自的執行上下文。 當函數代碼執行時,咱們說控制端進入了函數代碼的執行上下文; 當全局代碼執行時,咱們說控制端進入了全局代碼的執行上下文,以此類推。
正如你所見,執行上下文在邏輯上是一個棧(stack)。 首先可能有一段全局代碼,它擁有屬於本身的執行上下文; 在這段代碼中可能調用一個函數,這個函數一樣擁有屬於本身的執行上下文; 這個函數可能調用另外一個函數,等等。 即便當函數遞歸調用本身時,在每一步調用中仍然進入了不一樣的執行上下文。
每個執行上下文都有一個與之相關聯的變量對象(Variable object)。 和它類似的,變量對象也是一個抽象實體,一種用來描述變量實例化的機制。 而有趣的是,在一段源代碼中聲明的變量和函數事實上被做爲變量對象(Variable object)的屬性(properties)而添加到變量對象中。
當控制進入了全局代碼的執行上下文時,一個全局對象被用做變量對象。 這偏偏是爲何全局聲明的變量和函數變成一個全局對象的屬性的緣由:
var GLOBAL_OBJECT = this; var foo = 1; GLOBAL_OBJECT.foo; // 1 function bar() {}; typeof GLOBAL_OBJECT.bar; // "function" GLOBAL_OBJECT.bar === bar; // true
Ok, 因此全局變量成了全局函數的屬性,那麼局部變量——那些在函數代碼(Function code)中聲明的變量呢? 事實上那很簡單:他們也成了變量對象的屬性。 惟一的區別是,在函數代碼中,變量對象不是一個全局對象, 而是一個咱們稱之爲活化對象(Activation object)。 每次進入函數代碼的執行上下文時都會建立一個活化對象。
並不是只有在函數代碼中聲明的變量和函數才成爲活化對象的屬性: 函數的每個實參(arguments,以各自相對應的形參的名字爲屬性名), 以及一個特殊的Arguments對象(以arguments爲屬性名)一樣成爲了活化對象的屬性。 須要注意的是,活化對象做爲一個內部的機制事實上不能被程序代碼所訪問。
(function(foo) { var bar = 2; function baz() {}; /* 在抽象的過程當中, 特殊的'arguments'對象變成了所在函數的活化對象的屬性: ACTIVATION_OBJECT.arguments = arguments; ...參數'foo‘也是同樣: ACTIVATION_OBJECT.foo; // 1 ...變量'bar'也是同樣: ACTIVATION_OBJECT.bar; // 2 ...函數'baz'也是同樣: typeof ACTIVATION_OBJECT.baz; // "function" */ }) (1);
最後,Eval code 中聲明的變量成爲了上下文的變量對象(context's Variable object)的屬性。 Eval code 簡單地使用在它調用中的執行上下文的變量對象。
var GLOBAL_OBJECT = this; eval('var foo = 1'); GLOBAL_OBJECT.foo; // 1; (function() { eval('var bar = 2'); /* 在抽象過程當中 ACTIVATION_OBJECT.bar; // 2 */ }) ();
就要接近主題了。 如今咱們明確了變量發生了什麼(它們成了屬性),剩下的須要理解的概念就是屬性的內部屬性(property attributes)。 每個屬性擁有零至多個如內部屬性——*ReadOnly,DontEnum,DontDelete和Internal**。 你能夠把它們想象爲標籤——一個屬性可能擁有也可能沒有某個特殊的內部屬性。 在今天的討論中,咱們所感興趣的是 DontDelete。
當聲明變量和函數時,它們成爲了變量對象(Variable object)——要麼是活化對象(在函數代碼中), 要麼是全局對象(在全局代碼中)——的屬性,這些屬性伴隨生成了內部屬性 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 = "baz"; delete GLOBAL_OBJECT.baz; // true typeof GLOBAL_OBJECT.baz; // "undefined"
因此這就是全部這一切發生的緣由:屬性的一個特殊的內部屬性控制着該屬性是否能夠被刪除。 注意:內建對象的一些屬性擁有內部屬性 DontDelete,所以不能被刪除; 特殊的 arguments 變量(如咱們所知的,活化對象的屬性)擁有 DontDelete; 任何函數實例的 length (返回形參長度)屬性也擁有 DontDelete:
(function() { //不能刪除'arguments',由於有DontDelete delete arguments; // false; typeof arguments; // "object" //也不能刪除函數的length,由於有DontDelete function f() {}; delete f.length; // false; typeof f.length; // "number" }) ();
與函數 arguments 相關聯的屬性也擁有 DontDelete,一樣不能被刪除
(function(foo,bar) { delete foo; // false foo; // 1 delete bar; // false bar; // "bah" }) (1,"bah");
你可能記得,未聲明的變量賦值會成爲全局對象的屬性,除非這一屬性在做用域鏈內的其餘地方被找到。 而如今咱們瞭解了屬性賦值和變量聲明的區別——後者生成 DontDelete 而前者不生成——這也就是爲何未聲明的變量賦值能夠被刪除的緣由了。
var GLOBAL_OBJECT = this; /* 經過變量聲明生成全局對象的屬性,擁有DontDelete */ var foo = 1; /* 經過未聲明的變量賦值生成全局對象的屬性,沒有DontDelete */ bar = 2; delete foo; // false delete bar; // true
注意:內部屬性是在屬性生成時肯定的,以後的賦值過程不會改變已有的屬性的內部屬性。 理解這一區別是重要的。
/* 'foo'建立的同時生成DontDelete */ function foo() {}; /* 以後的賦值過程不改變已有屬性的內部屬性,DontDelete仍然存在 */ foo = 1; delete foo; // false; typeof foo; // "number" /* 但賦值一個不存在的屬性時,建立了一個沒有內部屬性的屬性,所以沒有DontDelete */ this.bar = 1; delete bar; // true; typeof bar; // "undefined"
那麼, firebug 中發生了什麼? 爲何在控制檯中聲明的變量可以被刪除,而不是想咱們以前討論的那樣? 我以前說過,Eval code 在它處理變量聲明時有一個特殊的行爲: 在 Eval code 中聲明的變量事實上生成一個沒有 DontDelete 的屬性。
eval('var foo = 1;'); foo; // 1 delete foo; // true typeof foo; // "undefined"
在函數代碼中也是同樣:
(function() { eval('var foo = 1;'); foo; // 1 delete foo; // true typeof foo; // "undefined" }) ();
而這就是 Firebug 中異常行爲的緣由了。 全部在控制檯中的調試文本彷佛是以 Eval code 來編譯和執行的,而不是在全局或函數代碼中執行。 顯然,其中的變量聲明最終都生成了不帶 DontDelete 的屬性,因此能夠被刪除。 因此要當心普通的全局代碼和 Firebug 控制檯中代碼的區別。
這個有趣的 eval 行爲,結合 ECMAScript 的另外一個方面能夠在技術上容許咱們刪除那些本來不能刪除的屬性。 這個方面是關於函數聲明——在相同的執行上下文中它們能覆蓋同名的變量:
function x() { }; var x; typeof x; // 「function」
那麼爲何函數聲明擁有優先權而能覆蓋同名變量(或者換句話說,變量對象(Variable object)的相同屬性)呢? 這是由於函數聲明的實例化過程在變量聲明以後,所以能夠覆蓋它們。
(譯者按:函數聲明只能覆蓋聲明而未賦值的同名變量,若是在聲明時賦值了值(e.g. var x = 1)則賦值值的過程在函數初始化以後,函數聲明反而被變量賦值所覆蓋,以下:)
var x = 1; function x() { }; typeof x; // "number"
函數聲明不止替換了屬性的值,同時也替換了它的內部屬性。 若是咱們經過 eval 來聲明函數,這一函數也將用它本身的內部屬性來替換以前的。 而因爲在 eval 中聲明的變量生成的屬性沒有 DontDelete, 實例化這個函數將在「理論上」移除原屬性已有的 DontDelete 內部屬性, 而使得這一屬性能夠刪除(固然,同時也將值指向了新生成的函數)。
var x = 1; /*不能刪除,‘x’擁有DontDelete*/ delete x; // false typeof x; // "number" eval('function x() { }'); /* 屬性'x'如今指向函數,而且應該沒有DontDelete */ typeof x; // "function" delete x; // 應該是‘true’; typeof x; // 應該是"undefined"
不幸的是,這種欺騙技術在我嘗試的各個瀏覽器中都沒有成功。 這裏我可能錯過了什麼,或者這個行爲太隱蔽而以致於各個瀏覽器沒有注意到它。
(譯者按:這裏的問題可能在於:函數聲明和變量聲明之間的覆蓋只是值指向的改變, 而內部屬性 DontDelete 則在最初聲明處肯定而再也不改變,而 eval 中聲明的變量和函數,也只是在其外部上下文中未聲明過的那部分才能被刪除。 關於執行順序,因爲 eval 做爲函數,它的調用永遠在其外部上下文中其餘變量和函數聲明以後, 所以相關的內部屬性也已肯定,覆蓋的只是值的指向。以下:)
/* 第一個 alert 返回 「undefined」,由於賦值過程在聲明過程和eval執行過程以後; 第二個alert返回 「false」, 由於儘管x聲明的位置在eval以後, 可是eval的執行卻在變量聲明以後,所以已沒法刪除 */ eval(' alert( x ); alert(delete x) '); var x = 1;
瞭解事物的工做原理是重要的,但實際的實現狀況更重要。 瀏覽器在建立和刪除變量/屬性時都遵照這些標準嗎? 對於大部分來講,是的。
我寫了一個簡單的測試單元來檢查全局代碼、函數代碼和Eval代碼的遵照狀況。 測試單元同時檢測了 delete 操做的返回值和屬性是否像預期那樣被刪除。 delete 的返回值並不像它的實際結果那樣重要,delete 操做返回 true 或 false 並不重要, 重要的是擁有/沒有 DontDelete 的屬性是否被刪除。
現代瀏覽器總的來講仍是遵照刪除規則的,如下瀏覽器所有經過測試: Opera 7.54+, Firefox 1.0+, Safari 3.1.2+, Chrome 4+。
Safari 2.x 和 3.0.4 在刪除函數 arguments 時存在問題,彷佛這些屬性在建立時不帶 DontDelete,所以能夠被刪除。 Safari 2.x 還有其餘問題——刪除無引用時(例如delete 1)拋出錯誤(譯者按:IE 一樣有); 函數聲明生成了可刪除的屬性(奇怪的是變量聲明則正常); eval 中的變量聲明變成不可刪除(而 eval 中的函數聲明則正常)。
與 Safari 相似,Konqueror(3.5,而非4.3)在 delete 無引用和刪除 arguments 是也存在一樣問題。
Gecko 1.8.x 瀏覽器—— Firefox 2.x, Camino 1.x, Seamonkey 1.x, etc. ——存在一個有趣的 bug:顯式賦值值給一個屬性能移除它的 DontDelete,即便該屬性經過變量或函數聲明而生成。
function foo() { }; delete foo; // false; typeof foo; // "function" this.foo = 1; delete foo; // true typeof foo; // "undefined"
使人驚訝的是,IE5.5-8 也經過了絕大部分測試,除了刪除非引用拋出錯誤(e.g. delete 1,就像舊的 Safari)。 可是,雖然不能立刻發現,事實上 IE 存在更嚴重的 bug,這些 bug 是關於全局對象。
在 IE 中(至少在 IE6-8 中),下面的表達式拋出異常(在全局代碼中):
this.x = 1; delete x; // TypeError: Object doesn't support this action
而下面則是另外一個:
var x =1; delete this.x; // TypeError: Cannot delete 'this.x' // 譯者按:在IE8下拋出此異常,在IE6,7下拋出的是和上面同樣的異常
這彷佛說明,在 IE 中在全局代碼中的變量聲明並無生成全局對象的同名屬性。 經過賦值建立的屬性(this.x = 1)而後經過 delete x 刪除時拋出異常; 經過變量聲明(var x = 1)建立的屬性而後經過 delete this.x 刪除時拋出另外一個(譯者按:在 IE6,7 下錯誤信息與上面的相同)。
但不僅是這樣,事實上經過顯式賦值建立的屬性在刪除時老是拋出異常。 這不僅是一個錯誤,而是建立的屬性看上去擁有了 DontDelete 內部屬性,而按規則應該是沒有的:
this.x = 1; delete this.x; // TypeError: Object doesn't support this action delete x; // TypeError: Object doesn't support this action
另外一方面,未聲明的變量賦值(那些一樣生成全局對象的屬性)又確實在IE下可以正常刪除:
x = 1; delete x; // true
但若是你試圖經過 this 關鍵字來進行刪除(delete this.x),那麼上面的異常又將拋出:
x = 1; delete this.x; //TypeError: Cannot delete 'this.x'
若是概括一下,咱們將發如今全局代碼中‘delete this.x’永遠不會成功。 當經過顯式賦值來生成屬性(this.x = 1)時拋出一個異常; 當經過聲明/非聲明變量的方式(var x = 1 or x = 1)生成屬性時拋出另外一個異常。 而另外一方面,delete x 只有在顯示賦值生成屬性(this.x = 1)時才拋出異常。
在 9 月我討論了這個問題,其中 Garrett Smith 認爲在 IE 中全局變量對象(Global variable object)實現爲一個 JScript 對象,而全局對象則由宿主對象實現。
咱們能經過幾個測試來在某種程度上確認這一理論。 注意,this 和 window 彷佛引用同一個對象(若是 ‘===’運算符能夠信任的話), 而變量對象 Variable object (函數聲明的基礎)則與 this 引用的不一樣。
function getBase() { return this; }; getBase() === this.getBase(); // false this.getBase() === this.getBase(); // true window.getBase() === this.getBase(); // true window.getBase() === getBase(); // false
咱們不能低估理解事物工做原理的重要性。 我看過網絡上一些關於 delete 操做的誤解。 例如,Stackoverflow 上的一個答案(並且等級還很高),裏面解釋說「delete is supposed to be no-op when target isn’t an object property」。 如今咱們瞭解了 delete 操做的核心,也就清楚了這個答案是不正確的。 delete 不區分變量和屬性(事實上在 delete 操做中這些都是引用),而只關心 DontDelete(以及屬性是否已經存在)。
一個 delete 的算法大體像這樣:
1. 若是運算元(operand)不是引用,返回 true 2. 若是對象沒有同名的**直接屬性**,返回 true (如咱們所知,對象能夠是全局對象也能夠是活化對象) 3. 若是屬性已經存在但有 DontDelete,返回 false 4. 不然,刪除移除屬性並返回 true
然而,對於宿主對象(host object)的 delete 操做的行爲卻多是不可預料的。 而事實上這並無錯:宿主對象(經過必定規則)容許實現任何操做, 例如讀(內部[[Get]]方法)、寫(內部[[Write]]方法)、刪除(內部[[Delete]]方法),等等。 這種容許自定義[[Delete]]行爲致使了宿主對象的混亂。
咱們已經看到了在IE中的一些問題:當刪除某些對象(那些實現爲了宿主對象)屬性時拋出異常。 一些版本的 firefox 當試圖刪除 window.location 時拋出異常(譯者按:IE 一樣拋出)。 一樣,在一些宿主對象中你也不能相信 delete 的返回值, 例以下面發生在 firefox 中的(譯者按:chrome 中一樣結果;IE 中拋出異常;opera 和 safari 容許刪除,而且刪除後沒法調用,姑且算’正常‘,儘管,從下面的討論來看彷佛倒是不正常的,它們事實上刪除了不能刪除的屬性,而前面的瀏覽器沒有):
/* 'alert'是’window‘的一個直接屬性(若是咱們可以相信'hasOwnProperty') */ window.hasOwnProperty('alert'); // true delete window.alert; // true typeof window.alert; // "function"
delete window.alert 返回 true,儘管這個屬性沒有任何條件可能產生這個結果(按照上面的算法): 它解析爲一個引用,所以不能在第一步返回 true; 它是 window 對象的直接屬性,所以不能在第二步返回 true; 惟一能返回 true 的是當算法達到最後一步同時確實刪除這個屬性,而事實上它並無被刪除。 (譯者按:不,在 opera 和 safari 中確實被刪除了...)。
因此這個故事告訴咱們永遠不要相信宿主對象。
那麼 ECMAScript 第 5 版中的嚴格模式將帶來什麼? 目前介紹了其中的一些限制。 當刪除操做指向一個變量/函數參數/函數聲明的直接引用時拋出 SyntaxError。 此外,若是屬性擁有內部屬性[[Configurable]] == false,將拋出 TypeError:
(function(foo) { "use strict"; //在函數中開啓嚴格模式 var bar; function baz; delete foo; // SyntaxError,當刪除函數參數時 delete bar; // SyntaxError,當刪除變量時 delete baz; // SyntaxError,當刪除由函數聲明建立的變量時 /* function實例的length擁有[[Configurable]] : false */ delete (function() {}).length; // TypeError }) ();
並且,在嚴格模式下,刪除未聲明的變量(換句話說,未解析的引用),一樣拋出 SyntaxError; 與它相似的,相同模式下未聲明的賦值也將拋出異常(ReferenceError)
"use strict"; delete i_dont_exist; // SyntaxError i_dont_exist_either = 1; // ReferenceError
看了以前給出的變量、函數聲明和參數的例子,相信如今你也理解了,全部這些限制都是有其意義的。 嚴格模式採起了更積極的和描述性的措施,而不僅是忽略這些問題。
因爲這篇文章已經很長了,所以我就再也不討論另外一些內容(e.g.經過 delete 刪除數組項及其影響)。 你能夠翻閱 MDC/MDN 上的文章或閱讀規範而後本身測試。
下面是關於 Javascript 中 delete 如何工做的一個簡單的總結: