下面我想要介紹的是一個(gè)謙卑的驚世之作。
在 1995 年布蘭登·艾克設(shè)計(jì) JavaScript 的第一個(gè)版本時(shí),他得到了許多錯(cuò)誤,包括有些從那以后已經(jīng)成為語言的一部分的東西,比如如果你不慎把 Date object 和 objects 相乘,它會(huì)自動(dòng)轉(zhuǎn)換成 NaN。然而,他所做對(duì)的事情在事后來看是極其重要的:對(duì)象、原型、有詞法作用域的一流功能、可變性默認(rèn)。語言具有良好的骨骼框架。這比以前實(shí)現(xiàn)的任何語言都要好。
不過,布蘭登做了一個(gè)特殊的決策,也是本章的重點(diǎn)內(nèi)容——一個(gè)我認(rèn)為可以被公開定性為錯(cuò)誤的決定。這是一個(gè)小事情,一個(gè)微妙的事情。你可能已經(jīng)使用這個(gè)語言多年但從未注意到它。但是它很重要,因?yàn)檫@個(gè)錯(cuò)誤存在于現(xiàn)在我們所認(rèn)為的“好的部分”里面。
它必須與變量一起工作。
這個(gè)規(guī)則聽起來很無辜。在一個(gè) JS 函數(shù)內(nèi)聲明的一個(gè) var 的作用域是那個(gè)函數(shù)的整個(gè)主體。但是這里有兩個(gè)方式可能讓它產(chǎn)生無病呻吟的效果。
一是在塊中聲明的變量的作用域不只是這個(gè)塊。它是整個(gè)函數(shù)。
你可能從未意識(shí)到這個(gè)。我恐怕這是你容易忽略的東西之一。我們來看一個(gè)場(chǎng)景,它導(dǎo)致了一個(gè)棘手的問題。
你現(xiàn)在的代碼中有一個(gè)變量,名為 t:
function runTowerExperiment(tower, startTime) {
var t = startTime;
tower.on("tick", function () {
... code that uses t ...
});
... more code ...
}
到目前為止一切都很好?,F(xiàn)在你想要添加保齡球速度測(cè)量,所以你在內(nèi)部回調(diào)函數(shù)中加入了一個(gè)if-語句。
function runTowerExperiment(tower, startTime) {
var t = startTime;
tower.on("tick", function () {
... code that uses t ...
if (bowlingBall.altitude() <= 0) {
var t = readTachymeter();
...
}
});
... more code ...
}
哦,親愛的。你在不知不覺中添加了第二個(gè)名為 t 的變量?,F(xiàn)在來看,之前工作地好好的“變量 t”指向新的內(nèi)部變量,而不再是現(xiàn)有的外部變量。
JavaScript 中 var 的作用域就像是 Photoshop 中的油漆桶工具。它同時(shí)向聲明的前后兩個(gè)方向延伸,直到到達(dá)一個(gè)函數(shù)邊界。因?yàn)檫@個(gè)變量 t 的作用域向后延伸的太長(zhǎng)了,所以我們?cè)谳斎牒瘮?shù)時(shí)必須盡快創(chuàng)建它。這就叫做變量提升(hoisting)。我喜歡把這想象成 JS 引擎利用一個(gè)微小的代碼起重機(jī)把每個(gè) var 和函數(shù)提升到封閉函數(shù)頂。
現(xiàn)在,變量提升有了它的優(yōu)點(diǎn)。沒有了它,許多在全球作用域內(nèi)工作良好的 cromulent 技術(shù)就無法在IIFE內(nèi)工作。但在這種情況下,變量提升會(huì)帶來一個(gè)討厭的錯(cuò)誤:你所有用了 t 的計(jì)算都會(huì)產(chǎn)生 NaN。這很難追查,特別是當(dāng)你的代碼比這個(gè)玩具例子更長(zhǎng)的時(shí)候。
添加一個(gè)新的代碼塊會(huì)給該塊之前的代碼帶來一個(gè)難以理解的錯(cuò)誤。只有我有這樣的問題,還是這真的很奇怪?我們不想影響到前面的原因。
但是與第二個(gè) var 問題相比,這真的是小菜一碟。
你可以猜猜你運(yùn)行這個(gè)代碼時(shí)會(huì)發(fā)生什么。這非常容易想。
var messages = ["Hi!", "I'm a web page!", "alert() is fun!"];
for (var i = 0; i < messages.length; i++) {
alert(messages[i]);
}
如果你一直關(guān)注 ES6,你會(huì)知道我喜歡用 alert() 作為示例代碼。也許你也知道 alert() 是一個(gè)糟糕的 API。它是同步的,所以當(dāng)一個(gè) alert 可見時(shí),輸入的事件不會(huì)傳遞。你的 JS 代碼——其實(shí)是你的整個(gè)界面——在用戶點(diǎn)擊 OK 之前基本暫停了。
你在 web 頁(yè)面上做的所有的操作,都可以使用 alert() 作為錯(cuò)誤提示。我使用它是因?yàn)槲艺J(rèn)為所有這些相同的事情會(huì)讓 alert() 成為一個(gè)有效的調(diào)試工具。
不過,我可能會(huì)被說服去放棄所有不良行為…如果這意味著我可以做一只會(huì)說話的貓。
var messages = ["Meow!", "I'm a talking cat!", "Callbacks are fun!"];
for (var i = 0; i < messages.length; i++) {
setTimeout(function () {
cat.say(messages[i]);
}, i * 1500);
}
但有些事情是錯(cuò)誤的。貓會(huì)說三次“未定義”,而不是依次說出這三條信息。
你能找出下圖的一個(gè) “bug” 嗎?
http://wiki.jikexueyuan.com/project/es-six-deeply/images/const.jpg" alt="" />圖片來源:內(nèi)維爾·賽沃瑞"
這里的問題是只有一個(gè)變量 i。它被循環(huán)本身和三個(gè)超時(shí)回調(diào)共享。當(dāng)循環(huán)完成運(yùn)行,i 的值是 3(因?yàn)?messages.length 是 3)而且所有的回調(diào)函數(shù)都還沒有被回調(diào)。
所以當(dāng)?shù)谝粋€(gè)超時(shí)觸發(fā)并調(diào)回 cat.say(messages[i]) 是用了未被定義的 messages[3]。
解決這個(gè)問題的方法有許多(這里有一個(gè)),但這是一個(gè) var 作用域規(guī)則帶來的二次問題。如果從一開始就沒有過這種問題,那便真是太好了。
在大多數(shù)情況下,JavaScript(還有其他編程語言,但尤其是 JavaScript)中的設(shè)計(jì)錯(cuò)誤無法被修復(fù)。向后兼容性意味著永遠(yuǎn)不改變現(xiàn)有的 JS 代碼在網(wǎng)絡(luò)上的行為。即使是標(biāo)準(zhǔn)委員會(huì)也沒有辦法修復(fù) JavaScript 自動(dòng)插入分號(hào)這件怪事。瀏覽器制造商根本無法實(shí)現(xiàn)突破性改革,因?yàn)槟欠N改變根本就是虐待他們的用戶。
所以大約十年前 Brendan Eich 決定解決這個(gè)問題時(shí),也就只有一個(gè)方法。
他添加了新的關(guān)鍵詞 let,let 就像 var 一樣可用來聲明變量,但它擁有更好的作用域規(guī)則。
它看起來是這樣的:
let t = readTachymeter();
let 和 var 是不同的,如果你只在你的代碼中做了一個(gè)全局搜索替換,可能就會(huì)由于 var 的原因破壞了你的部分代碼(可能是無意地)。但在大多數(shù)情況下,在新的 ES6 代碼中你應(yīng)該使用 let 代替 var。所以我們的口號(hào)是:“l(fā)et 是新型 var”。
let 和 var 之間的真正區(qū)別是什么?很高興你問了這個(gè)問題!
let 變量是有塊作用域的。用 let 定義的變量作用域只是封閉塊,而不是整個(gè)封閉函數(shù)?,F(xiàn)在仍有 let 變量提升,但是不再是隨意提升。在 TherunTowerExperiment 例子中,可以用把 var 替換成 let 的方法來進(jìn)行簡(jiǎn)單修復(fù)。如果你在每個(gè)地方都是使用的 let,你就不會(huì)有這種錯(cuò)誤。
全局 let 變量不是全局對(duì)象的屬性。也就是說,你不能通過寫 window.variableName 訪問它們。相反,它們存在于一個(gè)無形塊作用域內(nèi),我們可以想象成一個(gè)網(wǎng)頁(yè)運(yùn)行的所有的 JS 代碼都被封閉在了一起。
這是一個(gè)非常微妙的差異。這意味著如果一個(gè) for(let...)循環(huán)執(zhí)行多次并且該循環(huán)包含一個(gè)閉包,就像我們之前舉的會(huì)說話的貓的例子,每個(gè)閉包都會(huì)捕獲一個(gè)不同副本的循環(huán)變量而不是每個(gè)閉包都捕獲相同的循環(huán)變量。所以示例會(huì)說話的貓也可以通過用 let 替換 var 來完成修復(fù)。
這適用于所有三種 for 循環(huán):for–of,for–in 和加上分號(hào)的老式 C 類。
function update() {
console.log("current time:", t); // ReferenceError
...
let t = readTachymeter();
}
這個(gè)規(guī)則是幫助你定位錯(cuò)誤。你會(huì)在有問題的代碼上獲得一個(gè)提醒而不是結(jié)果 NaN。
未初始化變量在作用域內(nèi)的時(shí)期稱為暫時(shí)性死區(qū)。我一直在等待這個(gè)激發(fā)性的術(shù)語可以躍位到科幻小說。但是什么都還沒有發(fā)生。
(脆脆的性能細(xì)節(jié):在大多數(shù)情況下,你可以說出是否聲明已經(jīng)運(yùn)行了或只是在查看代碼,所以 JavaScript 引擎真的不需要在每次變量訪問時(shí)都進(jìn)行一次額外檢查來確保它已經(jīng)初始化了。然而有時(shí)在閉包內(nèi)這并不清楚。在這種情況下 JavaScript 引擎會(huì)進(jìn)行運(yùn)行時(shí)檢查。這代表與 var 相比 let 需要更長(zhǎng)的訪問時(shí)間。)
(交替全局作用域細(xì)節(jié):在某些程序設(shè)計(jì)語言中,變量的作用域在聲明點(diǎn)開始,而不是到達(dá)后面覆蓋整個(gè)封閉塊。標(biāo)準(zhǔn)委員考慮過為 let 使用這個(gè)作用域規(guī)則。但這樣使用 t 就會(huì)導(dǎo)致引用錯(cuò)誤,隨后的 let t 就不在作用域中了,所以它根本不會(huì)再查閱那個(gè)變量。它可以在一個(gè)封閉作用域中查閱 t,但這個(gè)方法在有閉包或有函數(shù)變量提升的情況下不好用,所以它最終被拋棄了。)
這條規(guī)則也有助于你發(fā)現(xiàn)微小錯(cuò)誤。但這是在你嘗試 let-to-var 轉(zhuǎn)換時(shí)容易導(dǎo)致問題的不同之處,因?yàn)樗峁┝松踔寥值?let 變量。
如果你有一些都是聲明相同全局變量的腳本,你最好使用 var。如果你用了 let,任何腳本在二次加載的時(shí)候都會(huì)出現(xiàn)錯(cuò)誤無法運(yùn)行。
或者運(yùn)用 ES6 模塊。但這又是另一個(gè)故事了。
(語法細(xì)節(jié):let 是一個(gè)嚴(yán)格模式下的保留詞。在非嚴(yán)格模式的代碼中,為了向后兼容,你仍然可以聲明變量、函數(shù)和名為 let 的實(shí)參——你可以寫 var let = 'q';!,而不是你原本想寫的那樣。let let; 也是不允許的。)
除了這些差異,let 和 var 幾乎是一樣的。它們都支持聲明多個(gè)用逗號(hào)隔開的變量,例如他們都支持解構(gòu)。
注意,類聲明的行為和 let 很像而和 var 不像。如果你加載了一個(gè)包含多次類的腳本,在第二次時(shí)你會(huì)因?yàn)闆]有重新聲明類而得到一個(gè)錯(cuò)誤。
對(duì)了,還有一件事!
ES6 還引入了第三級(jí)關(guān)鍵字,你可以在 let 后使用:const。
用 const 聲明的變量就像 let,除非你不能指定它們或除非你在它們的聲明點(diǎn)進(jìn)行了聲明,這樣會(huì)產(chǎn)生語法錯(cuò)誤。
const MAX_CAT_SIZE_KG = 3000; //