[譯] 理解 JavaScript 中的 undefined

position_柚子發表於2018-11-21

與其他的語言相比,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?

是的,一般都很好理解,但是還需要重申的是:undefinednull 不同,null 表示有意的缺少值的原始值。undefinednull 唯一的相似之處是,它們都為 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 來檢查引用錯誤。

我在哪裡可以閱讀更多?

Mozilla 開發者中心:undefined

Angus Croll:JavaScript 中的變數與屬性

Juriy Zaytsev (“kangax”):理解 Delete

Dmitry A. Soshnikov:ECMA-262-3 詳解:第 2 章 Variable 物件

ECMA-262 第五版標準文件

undefined:4.3.9, 4.3.10, 8.1

Reference Error:8.7.1, 8.7.2, 10.2.1, 10.2.1.1.4, 10.2.1.2.4, and 11.13.1.

ECMAScript 的嚴格模式 Annex C

如果發現譯文存在錯誤或其他需要改進的地方,歡迎到 掘金翻譯計劃 對譯文進行修改並 PR,也可獲得相應獎勵積分。文章開頭的 本文永久連結 即為本文在 GitHub 上的 MarkDown 連結。


掘金翻譯計劃 是一個翻譯優質網際網路技術文章的社群,文章來源為 掘金 上的英文分享文章。內容覆蓋 AndroidiOS前端後端區塊鏈產品設計人工智慧等領域,想要檢視更多優質譯文請持續關注 掘金翻譯計劃官方微博知乎專欄

相關文章