在這篇文章中作者從《JavaScript物件導向程式設計指南》一書中關於delete 的錯誤講起,詳細講述了關於delete 操作的實現, 局限以及在不同瀏覽器和插件(這裡指firebug)中的表現。
下面翻譯其中的主要部分。
...書中聲稱
「函數就像一個普通的變數-可以拷貝到不同變量,甚至被刪除」
並附上了下面的程式碼片段作為說明:
你能發現片段中的問題嗎? 這個問題就是-刪除 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 文件幾乎沒什麼用處。
一、理論| Theory
那麼,為什麼我們能刪除一個物件的屬性:
1.1、代碼的類型 | Type of code
ECMAScript 中有三類可執行程式碼:
1.全域代碼 Global code
2.函數代碼 Function code
3.Eval code
這幾類的意思大致就像它們命名的那樣,但還是快速地回顧一下:
1.當一個原始檔被看做是一個程序,它在全域作用域(scope)內執行,而這就被認為是一段全域程式碼 Global code。 在瀏覽器環境下,SCRIPT 元素的內容通常都會被解析為一個程序,因而作為全域程式碼來執行。
2.當然,任何在一段函數中直接執行的程式碼就被認為是一段函數程式碼Function code, 在瀏覽器環境下,事件屬性的內容(e.g.
3.最後,放入內建函數 eval 中的程式碼就以 Eval code 來解析。 我們很快就會看到為什麼這種類型是特殊的。 1.2、程式碼執行的上下文 | Execution Context 如你所見,執行上下文在邏輯上是一個堆疊(stack)。 首先可能有一段全域程式碼,它擁有屬於自己的執行上下文; 在這段程式碼中可能會呼叫一個函數,這個函數同樣擁有屬於自己的執行上下文; 這個函數可能會呼叫另一個函數,等等。 即使當函數遞歸呼叫自己時,在每一步呼叫中仍然進入了不同的執行上下文。 1.3、活化物件與變數物件 | Activation object / Variable object 當控制項進入了全域程式碼的執行上下文時,一個全域物件被用作變數物件。 這正是為什麼全域宣告的變數和函數變成一個全域物件的屬性的原因:
當 ECMAScript 程式碼執行時,它總是發生在一個確定的執行上下文(context)中。 執行作用域是一個抽象實體,它有助於理解作用域和變數實例化的工作原理。 上面三類可執行程式碼都有各自的執行上下文。 當函數程式碼執行時,我們說控制端進入了函數程式碼的執行上下文; 當全域程式碼執行時,我們說控制端進入了全域程式碼的執行上下文,以此類推。
每一個執行上下文都有一個與之相關聯的變數物件(Variable object)。 和它相似的,變數物件也是一個抽象實體,一種用來描述變數實例化的機制。 而有趣的是,在一段原始程式碼中宣告的變數和函數事實上被當作變數物件(Variable object)的屬性(properties)而加入到變數物件中。
var GLOBAL_OBJECT = this;
var foo = 1;
GLOBAL_OBJECT.foo; // 1
function bar() {}
OB; / "function"
GLOBAL_OBJECT.bar === bar; // true
Ok, 所以全域變數成了全域函數的屬性,那麼局部變數──那些在函數程式碼(Function code)中宣告的變數呢? 事實上那很簡單:他們也成了變數物件的屬性。 唯一的差異是,在函數程式碼中,變數物件不是一個全域對象, 而是一個我們稱之為活化對象(Activation object)。 每次進入函數程式碼的執行上下文時都會建立一個活化物件。
並非只有在函數程式碼中宣告的變數和函數才成為活化物件的屬性: 函數的每一個實參(arguments,以各自相對應的形參的名字為屬性名稱), 以及一個特殊的Arguments對象(以arguments為屬性名)同樣成為了活化物件的屬性。 需要注意的是,活化物件作為一個內部的機制事實上不能被程式碼所存取。
ACTIVATION_OBJECT.bar; // 2
就要接近主題了。 現在我們明確了變數發生了什麼事(它們成了屬性),剩下的需要理解的概念就是屬性的內在屬性(property attributes)。 每一個屬性擁有零至多個如內部屬性-*ReadOnly,DontEnum,DontDelete和Internal**。 你可以把它們想像為標籤──一個屬性可能擁有也可能沒有某個特殊的內在屬性。 在今天的討論中,我們所感興趣的是 DontDelete。
當宣告變數和函數時,它們成為了變數物件(Variable object)-要麼是活化物件(在函數程式碼中), 要麼是全域物件(在全域程式碼中)—的屬性,這些屬性伴隨產生了內部屬性DontDelete。 然而,任何顯式/隱式賦值的屬性不產生 DontDelete。 而這就是本質上為什麼我們能刪除一些屬性而不能刪除其他的原因。
複製程式碼
程式碼如下:
/* '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"
1.5、內建與DontDelete | Build-ins and DontDelete
所以這就是所有這一切發生的原因:屬性的一個特殊的內部屬性控制著該屬性是否可以被刪除。 注意:內建物件的一些屬性擁有內部屬性DontDelete,因此不能被刪除; 特殊的arguments 變數(如我們所知的,活化物件的屬性)擁有DontDelete;任何函數實例的length (返回形參長度)屬性也擁有DontDelete:
程式碼如下:
delete f.length; // false;
}) ();
與函數 arguments 相關聯的屬性也擁有 DontDelete,同樣不能被刪除
delete bar; // false
bar; // "bah"
}) (1,"bah");
1.6、未宣告的變數賦值 | Undeclared assignments
你可能記得,未宣告的變數賦值會成為全域物件的屬性,除非這個屬性在作用域鏈內的其他地方被找到。 而現在我們了解了屬性賦值和變數宣告的差異——後者產生 DontDelete 而前者不產生——這也就是為什麼未宣告的變數賦值可以被刪除的原因了。
var GLOBAL_OBJECT = this
this;
var foo = 1;
bar = 2;
delete foo; // false
注意:內部屬性是在屬性產生時決定的,之後的賦值過程不會改變現有的屬性的內部屬性。 理解這一區別是重要的。
function foo() {};
/* 之後的賦值過程不會改變已有屬性的內部屬性,DontDelete仍然存在*/
foo = 1;
typeof foo; // "number "
/* 但賦值一個不存在的屬性時,創建了一個沒有內部屬性的屬性,因此沒有DontDelete */
this.bar = 1;
delete bar; // true;
二、Firebug 的混亂 | Firebug confusion
那麼, firebug 中發生了什麼事? 為什麼在控制台中聲明的變數能夠被刪除,而不是想我們之前討論的那樣? 我之前說過,Eval code 在它處理變數宣告時有一個特殊的行為: 在 Eval code 中宣告的變數事實上會產生一個沒有 DontDelete 的屬性。
在函數程式碼中也是一樣:
of typedef foo> ();
function x() { }
var x ;
函數宣告不只替換了屬性的值,同時也替換了它的內部屬性。 如果我們透過 eval 來宣告函數,這一函數也會用它自己的內部屬性來取代先前的。 而由於在eval 中宣告的變數所產生的屬性沒有DontDelete, 實例化這個函數將在「理論上」移除原屬性已有的DontDelete 內部屬性, 而使得這一屬性可以刪除(當然,同時也將值指向了新產生的函數)。
eval('function x() { }');
/* 屬性'x'現在指向函數,且應該沒有DontDelete */
typeof x; // "function"
delete x ; // 應該是'true';
typeof x; // 應該是"undefined"
不幸的是,這種欺騙技術在我嘗試的各個瀏覽器中都沒有成功。 這裡我可能錯過了什麼,或者這個行為太隱蔽而以至於各個瀏覽器沒有註意到它。
(譯者按:這裡的問題可能在於:函數宣告和變數宣告之間的覆蓋只是值指向的改變, 而內部屬性DontDelete 則在最初聲明處確定而不再改變,而eval 中宣告的變量和函數,也只是在其外部上下文中未聲明過的那部分才能被刪除。也已確定,覆蓋的只是值的指向。 🎜>/* 第一個alert 返回“undefined”,因為賦值過程在聲明過程和eval執行過程之後;
第二個alert返回“false”, 因為儘管x聲明的位置在eval之後,
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,即使屬性透過變數或函數宣告而產生。
複製程式碼 程式碼如下:
令人驚訝的是,IE5.5-8 也通過了絕大部分測試,除了刪除非引用拋出錯誤(e.g. delete 1,就像舊的 Safari)。 但是,雖然不能馬上發現,事實上 IE 有更嚴重的 bug,這些 bug 是關於全域物件。
四、IE bugs
在IE 中(至少在IE6-8 中),下面的表達式拋出異常(在全域程式碼中):
而下面則是另一個:
這似乎說明,在 IE 中在全域程式碼中的變數宣告並沒有產生全域物件的同名屬性。 透過賦值建立的屬性(this.x = 1)然後透過delete x 刪除時拋出異常; 透過變數宣告(var x = 1)建立的屬性然後透過delete this.x 刪除時拋出另一個(譯者按:在IE6,7 下錯誤訊息與上面的相同)。
但不只是這樣,事實上透過明確賦值所建立的屬性在刪除時總是會拋出異常。 這不只是一個錯誤,而是創建的屬性看起來擁有了 DontDelete 內部屬性,而規則應該是沒有的:
如果歸納一下,我們將發現在全域程式碼中‘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 對象,而全域物件則由全域物件宿主對象實作。
function getBase() { return this; >
getBase() === this.getBase(); // false
this.getBase() === this.getBase(); // true
window.getBase() === this .getBase(); // true
五、誤解 | Misconceptions
我們不能低估理解事物運作原理的重要性。 我看過網路上一些關於 delete 操作的誤解。 例如,Stackoverflow 上的一個答案(而且等級還很高),裡面解釋說「delete is supposed to be no-op when target isn't an object property」。 現在我們了解了 delete 操作的核心,也就清楚了這個答案是不正確的。 delete 不區分變數和屬性(事實上在 delete 操作中這些都是引用),而只關心 DontDelete(以及屬性是否已經存在)。
六、'delete'和宿主物件 | 'delete‘ and host object
一個 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 允許刪除,並且刪除後無法調用,姑且算'正常',儘管,從下面的討論來看似乎卻是不正常的,它們事實上刪除了不能刪除的屬性,而前面的瀏覽器沒有):
delete window.alert; // true
typeof window.alert; // "function"
delete window.alert 傳回true,儘管這個屬性沒有任何條件可能產生這個結果(按照上面的演算法): 它解析為一個引用,因此不能在第一步傳回true; 它是window 物件的直接屬性,因此不能在第二步回傳true;唯一能回傳true 的是當演算法達到最後一步同時確實刪除這個屬性,而事實上它並沒有被刪除。 (譯者按:不,在 opera 和 safari 中確實被刪除了...)。
所以這個故事告訴我們永遠不要相信宿主物件。
七、ES5 嚴格模式 | ES5 strict mode
那麼 ECMAScript 第 5 版中的嚴格模式將帶來什麼? 目前介紹了其中的一些限制。 當刪除操作指向一個變數/函數參數/函數宣告的直接引用時拋出 SyntaxError。 此外,如果屬性擁有內部屬性[[Configurable]] == false,將會拋出 TypeError:
(function(foo) {
" "; //在函數中開啟嚴格模式
var bar;
function baz;
delete bar; ,當刪除由函數宣告建立的變數時
/* function實例的length擁有[[Configurable]] : false */
delete (function() {}).length; // TypeError
而且,在嚴格模式下,刪除未聲明的變數(換句話說,未解析的引用),同樣拋出SyntaxError; 與它類似的,相同模式下未聲明的賦值也將拋出異常(ReferenceError )
"use strict";
dgfiddon/tendo; / SyntaxErrori_dont_exist_either = 1; // ReferenceError
看了之前給出的變數、函數宣告和參數的例子,相信現在你也理解了,所有這些限制都是有其意義的。 嚴格模式採取了更積極的和描述性的措施,而不只是忽略這些問題。
八、總結 | Summary由於這篇文章已經很長了,因此我就不再討論另一些內容(e.g.透過 delete 刪除數組項及其影響)。 你可以翻閱
MDC/MDN 上的文章
或閱讀規格然後自己測驗。
以下是關於 Javascript 中 delete 如何運作的一個簡單的總結:
•變數和函數宣告都是活化(Activation)全域(Global)物件的屬性。
•全域程式碼或函數程式碼中的變數、函數宣告都會產生擁有 DontDelete 的屬性。 •函數參數同樣是活化物件的屬性,也擁有 DontDelete。 •Eval程式碼中的變數和函數宣告都會產生沒有 DontDelete 的屬性。 •新的未宣告的屬性在產生時帶空的內部屬性,因此也沒有 DontDelete。 •宿主物件允許以任何它們期望的方式來回應刪除過程。 原文:
Understanding delete譯:javascript 的 delete譯者:justjavac