【譯】理解 JavaScript 中的 undefined
與其他的語言相比,JavaScript 中 undefined 的概念是有些令人困惑的。特別是試圖去理解 ReferenceError(“x is not defined”)以及如何針對它們寫出優雅的程式碼是很令人沮喪的。
本文是我試圖把這件事情弄清楚的一些嘗試。如果你還不熟悉 JavaScript 中變數和屬性的區別(包括內部的 VariableObject),那麼最好先去閱讀一下我的上一篇文章。
什麼是 undefined?
在 JavaScript 中有 Undefined (type)、undefined (value) 和 undefined (variable)。
Undefined (type)是 JavaScript 的內建型別。
undefined (value)是 Undefined 型別的唯一的值。任何未被賦值的屬性都被假定為undefined
(ECMA 4.3.9 和 4.3.10)。沒有 return 語句的函式,或者 return 空的函式將返回 undefined。函式中沒有被定義的引數的值也被認為是 undefined。
var a; typeof a; //"undefined" window.b; typeof window.b; //"undefined" var c = (function() {})(); typeof c; //"undefined" var d = (function(e) {return e})(); typeof d; //"undefined" 複製程式碼
undefined (variable)是一個初始值為 undefined (value) 的全域性屬性,因為它是一個全域性屬性,我們還可以將其作為變數訪問。為了保持一致性,我在本文中統一稱它為變數。
typeof undefined; //"undefined" var f = 2; f = undefined; //re-assigning to undefined (variable) typeof f; //"undefined" 複製程式碼
從 ECMA 3 開始,它可以被重新賦值:
undefined = "washing machine"; //把一個字串賦值給 undefined (變數) typeof undefined //"string" f = undefined; typeof f; //"string" f; //"washing machine" 複製程式碼
毋庸置疑,給 undefined 變數重新賦值是非常不好的做法。事實上,ECMA 5 不允許這樣做(不過,在當前的瀏覽器中,只有 Safari 強制執行了)。
然後是 null?
是的,一般都很好理解,但是還需要重申的是:undefined
與null
不同,null
表示有意的
缺少值的原始值。undefined
和null
唯一的相似之處是,它們都為 false。
所以,什麼是 ReferenceError(引用錯誤)?
ReferenceError 說明檢測到了一個無效的引用值。(ECMA 5 15.11.6.3)
在實際專案中,這意味著當 JavaScript 試圖獲取一個不可被解析的引用時,會丟擲 ReferenceError。(還有一些其他的情況會丟擲 ReferenceError,尤其是在 ECMA 5 嚴格模式下執行的時候。如果你有興趣的話,可以看本文末尾的閱讀列表。)
需要注意不同瀏覽器發出的訊息語法是如何變化的,正如我們將看到的,這些資訊沒有一個是特別有啟發性的:
alert(foo) //FF/Chrome: foo is not defined //IE: foo is undefined //Safari: can't find variable foo 複製程式碼
仍然不清楚“無法解析的引用(unresolvable reference)”?
在 ECMA 術語中,引用由基值(base value)和引用名(reference name)構成(ECMA 5 8.7 - 我再次忽略了嚴格模式。還要注意,ECMA 3 的術語略有不同,但實際意義是相同的)。
如果引用是屬性,那麼基值和引用名位於.
的兩側(或第一個括號或其他):
window.foo; //base value = window, reference name = foo; a.b; //base value = a, reference name = b; myObj['create']; // base value = myObj, reference name = 'create'; //Safari, Chrome, IE8+ only Object.defineProperty(window,"foo", {value: "hello"}); //base value = window, reference name = foo; 複製程式碼
對於變數引用,基值是當前執行上下文的 VariableObject。全域性上下文的 VariableObject 是全域性物件本身(瀏覽器中的window
)。每個函式上下文都有一個抽象的變數物件,稱為 ActivationObject。
var foo; //base value = window, reference name = foo function a() { var b; base value = <code>ActivationObject</code>, reference name = b } 複製程式碼
如果基值是 undefined,則認為引用是無法被解析的。
因此,如果在.
之前的變數值為 undefined,那麼屬性引用是不可被解析的。下面的示例本會丟擲一個 ReferenceError,但實際上它不會,因為 TypeError 會先被丟擲。這是因為屬性的基值受 CheckObjectCoercible (ECMA 5 9.10 到 11.2.1)的影響,在它嘗試將 Undefined 型別轉換為 Object 的時候會丟擲 TypeError。(感謝 kangax 在 twitter 上提前釋出的訊息)
var foo; foo.bar; //TypeError(基值,foo 是未定義的) bar.baz; //ReferenceError(bar 是不能被解析的) undefined.foo; //TypeError(基值是未定義的) 複製程式碼
變數引用永遠會被解析,因為 var 關鍵字確保 VariableObject 總是被賦給基值。
根據定義,既不是屬性也不是變數的引用是不可解析的,並且會丟擲一個 ReferenceError:
foo; //ReferenceError 複製程式碼
上面的 JavaScript 中沒有看到顯式的基值,因此會查詢 VariableObject 來引用名稱為foo
的屬性。確定foo
沒有基值,然後丟擲 ReferenceError。
但是foo
不是一個未宣告的變數嗎?
技術上不是的。雖然我們有時會發現 “undeclared variable” 是一個錯誤診斷時有用的術語,但實際上,在變數被宣告之前不是變數。
那麼隱式全域性變數呢?
的確,從未被 var 關鍵字宣告過的識別符號將被建立為全域性變數 —— 但只有當它們被賦值時才會這樣。
function a() { alert(foo); //ReferenceError bar = [1,2,3]; //沒有錯誤,foo 是全域性的 } a(); bar; //"1,2,3" 複製程式碼
當然,這很煩人。如果 JavaScript 在遇到無法解析的引用時始終丟擲 ReferenceErrors 那就更好了(實際上這是它在 ECMA 嚴格模式下所做的)。
什麼時候需要針對 ReferenceError 進行編碼?
如果你的程式碼寫得夠好的話,其實很少需要這樣做。我們已經看到,在典型的用法中,只有一種方法可以獲得不可解析的引用:使用既不是屬性也不是變數的僅在語法上正確的引用。在大多數情況下,確保記住 var 關鍵字可以避免這種情況。只有在引用只存在於某些瀏覽器或第三方程式碼中的變數時,才會出現執行時異常。
一個很好的例子是console 。在 Webkit 瀏覽器中,console 是內建的,console 的屬性總是可用的。然而 firefox 中的 console 依賴於安裝和開啟Firebug(或其他附加元件)。IE7 沒有 console,IE8 有 console,但 console 屬性只在 IE 開發工具啟動時存在。顯然 Opera 有 console,但我從來沒有使用過。
結論是,下面的程式碼片段在瀏覽器中執行時很可能會丟擲 ReferenceError:
console.log(new Date()); 複製程式碼
如何對可能不存在的變數進行編碼?
檢查一個不可解析的引用而且不丟擲 ReferenceError 的一種方法是使用typeof
關鍵字。
if (typeof console != "undefined") { console.log(new Date()); } 複製程式碼
然而,這在我看來總是很繁瑣的,更不用說可疑的了(它不是引用名稱是 undefined,而是基值為 undefined)。但是無論如何,我更喜歡保留typeof
來進行型別檢查。
幸運的是,還有另一種方法:我們已經知道,如果 undefined 屬性的基值被定義,那麼它就不會丟擲 ReferenceError —— 而且由於 console 屬於全域性物件,我們就可以這樣做:
window.console && console.log(new Date()); 複製程式碼
實際上,只需要檢查全域性上下文中是否存在變數(函式中存在其他執行上下文,而且你可以控制自己的函式中存在哪些變數)。所以,理論上你應該能夠避免使用typeof
來檢查引用錯誤。