深入理解 JavaScript 中的 With 語句
通常來說,所有 JavaScript 開發(fā)人員都有一個共同的概念:“避免使用 with 語句?!?這條準則無疑是正確的,但并不是每個人都能很好地解釋為什么。雖然只記住“不要使用它”的結(jié)果就足夠了,但理解其背后的原因?qū)τ谏钊肜斫?JavaScript 語言和編寫高質(zhì)量代碼非常有幫助。
with 語句
with 語句的初衷是為了避免冗長的對象調(diào)用:
foo.bar.baz.x = 1;
foo.bar.baz.y = 2;
foo.bar.baz.z = 3;
with(foo.bar.baz) {
x = 1;
y = 2;
z = 3;
}
但實際上,使用變量替換是相當簡單的:
var p = foo.bar.baz;
p.x = 1;
p.y = 2;
p.z = 3;
因此,看起來一開始就不需要 with。如今,使用 with 的人已經(jīng)很少了。在嚴格模式下,使用 with 會直接報錯:
function foo() {
'use strict';
with ({}) {}
}
因此,with 已經(jīng)被完全廢棄,人們甚至懶得去關(guān)注其原因。
書中的陳述
既然是總結(jié),我想盡可能全面,所以讓我們先從書籍開始。關(guān)于 JavaScript 的書籍,以下是一些主要參考:
《JavaScript 權(quán)威指南》(第 5 版,David Flanagan,P109):
with (Object) statement with 語句用于暫時修改作用域鏈… 這種語句實際上是將對象添加到作用域鏈的開頭,然后執(zhí)行語句,再將作用域鏈恢復到原來的狀態(tài)… 盡管有時使用 with 語句更方便,但人們反對使用它。使用 with 語句的 JavaScript 代碼難以優(yōu)化,因此其執(zhí)行速度比不使用 with 語句的等效代碼慢得多。此外,在 with 語句中定義函數(shù)和初始化變量可能會產(chǎn)生與直覺相悖的意外行為(這種行為及其原因非常復雜,我們在此不再解釋)。
《JavaScript 高級程序設(shè)計》(第 3 版,Nicholas C. Zakas,P60):
with 語句的目的是將代碼的作用域設(shè)置為特定對象… 由于頻繁使用 with 語句導致的性能下降,以及調(diào)試代碼的困難,不建議在開發(fā)大型應(yīng)用程序時使用 with 語句。
《JavaScript 語言精粹》(Douglas Crockford,P110):
這個語言中存在 with 語句嚴重影響了 JavaScript 處理器的速度,因為它破壞了變量名的詞法作用域綁定。它的初衷是好的,但如果沒有它,JavaScript 語言會稍微好一些。
《深入理解 ECMAScript 6》(Axel Rauschmayer,P153):
這本書是我唯一一本用了一頁多的篇幅詳細解釋了 JavaScript 中廢棄 with 的原因的基礎(chǔ)參考書。
好了,讀了這么多書,讓我們現(xiàn)在進入本文的主要話題:
為什么不使用 with 語句?
綜上所述,主要考慮如下:
性能問題
with 語句存在明顯的性能問題,這在幾乎所有參考書中都有提到,但很少有例子來說明這一點。你可以自己進行代碼測試,以更直觀地量化理解 with 語句的性能。
var a = {a: {a: 1}};
function useWith() {
with (a.a) {
for (var i = 0; i < 1000000; i++) {
a = i;
}
}
}
var b = {b: {b: 1}};
function noWith() {
for (var i = 0; i < 1000000; i++) {
b.b.b = i;
}
}
var t1 = new Date().getTime();
useWith();
alert(new Date().getTime() - t1);
var t2 = new Date().getTime();
noWith();
alert(new Date().getTime() - t2);
在對象屬性賦值一百萬次時,性能差異是否顯著?
當然,在實際使用中,極少有執(zhí)行數(shù)百萬次的循環(huán),損失在可接受范圍內(nèi)。因此,性能損失并不是廢棄 with 語句的主要原因。
不可預測性
使用 with 語句導致的不可預測性是廢棄 with 的根本原因。with 強行截斷詞法作用域,臨時將對象插入作用域鏈。這導致代碼變得難以捉摸。
例如:
function foo(a) {
with (a) {
console.log(a);
}
}
foo("sword"); // 輸出: sword
foo({}); // 輸出: [object Object]
foo({a: "sword"}); // 輸出: {a: "sword"}
在這個簡單例子中,字符串 "sword" 和空對象沒有問題。然而,當傳遞的參數(shù)是具有名為 a 的屬性的對象時,強行發(fā)生 a.a 訪問。
這只是一個參數(shù)的情況。如果有很多參數(shù)呢?當不知道傳入?yún)?shù)有什么屬性時,可以想象在多個參數(shù)之間引用各種屬性會有多么混亂。這就是所謂的“令人驚訝和違反直覺”的行為本質(zhì)。
此外,在 with 語句中聲明的變量并不屬于 with 指定的對象:
var a = {};
with (a) {
x = 'sword';
var y = 'wang';
}
console.log(a.x); // undefined
console.log(a.y); // undefined
console.log(window.x); // sword
console.log(window.y); // wang
在 with 中聲明的變量被添加到外部函數(shù)中。
function foo() {
with ({}) { x = 'sword'; }
console.log(x);
}
foo(); // 輸出: sword
這可能和你想象的有些不同。
單單通過標識符及其上下文,是無法確定語句中的標識符指向什么的。這才是 with 被棄用的真正原因。它強行混淆了上下文,使程序的預測和解析變得困難,導致了后面會討論的優(yōu)化問題。
代碼無法優(yōu)化
由于無法預測,代碼的含義不斷變化。不同的調(diào)用,甚至相同的調(diào)用,由于運行時的變化可能會偏離,使得代碼無法優(yōu)化。
優(yōu)化涉及兩個方面。一方面,解析和執(zhí)行變慢,這指的是前面提到的性能。另一方面,對于代碼優(yōu)化和壓縮工具,如果無法確定是否正在使用變量或?qū)傩?,則無法重命名(因為屬性無法重命名)。
總結(jié)
在這個炎熱的夏天,我可能被熱氣蒸得有些思維散亂。心血來潮,我翻出了幾本關(guān)于 JavaScript 的書,想要探討一下這個被廣泛詬病的 with 語句。說著說著,似乎偏離了主題,胡亂扯了一些看似深奧但不太實用的內(nèi)容。寫完之后,我自己都覺得“哇,這人真閑”。
哦,對了,文章開頭還有一個冷笑話,說 JavaScript 比 Java 多 60%。我只是在調(diào)侃它們的字符數(shù)?!癑avaScript” 比 Java 多五個字母;如果你堅持數(shù)字符,那么大概是多了 60%。好吧,可能這個笑話有點冷,難怪外面這么熱——看來我得冷靜一下。
寫這些東西可以算是一種消暑和消磨時間的方式。希望你讀到這里時,也能在這個夏季找到屬于你的涼爽享受。至于 with 語句——了解它并擱置一旁,因為我們反正不會用了,不是嗎?