溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務(wù)條款》

輕松理解Javascript變量的相關(guān)問題

發(fā)布時間:2020-10-11 21:44:05 來源:腳本之家 閱讀:151 作者:daisy 欄目:web開發(fā)

前言

再說本文的內(nèi)容之前,我們先回溯到1995年,當Brendan Eich在設(shè)計第一版JavaScript時,他搞錯了許多東西,當然這也包括曾屬于語言本身的一部分,例如Date對象,對象相乘被自動轉(zhuǎn)換為NaN等。然而現(xiàn)在回過頭看,語言最重要的部分都是設(shè)計合理的:對象、原型、具有詞法作用域的一等函數(shù)、默認情況下的可變性等。語言的骨架非常優(yōu)秀,甚至超越了人們對它的初步印象。

話說回來,正是Brendan當初的設(shè)計錯誤才誕生了今天這篇文章。我們這次關(guān)注的目標非常小,在你使用這門語言多年后可能根本不會注意到這個問題,但是它又如此重要,因為我們可能會誤認為這個錯誤就是語言設(shè)計中的“the good parts”(譯者注:請參考《JavaScript語言精粹》一書中附錄A:毒瘤中有關(guān)作用域的描述)。

今天我們一定要把這些與變量有關(guān)的問題拿下。

問題 #1:JS沒有塊級作用域

請看這樣一條規(guī)則: 在JS函數(shù)中的var聲明,其 作用域 是函數(shù)體的全部 。乍一聽沒什么問題,但是如果碰到以下兩種情況就不會得到令人滿意的結(jié)果。

其一,在代碼塊內(nèi)聲明的變量,其作用域是整個函數(shù)作用域而不是塊級作用域。

你之前可能沒有關(guān)注到這一點,但我擔心這個問題確實是你不能夠輕易忽視的。我們一起重現(xiàn)一下由這個問題引發(fā)的bug。

假如你現(xiàn)在的代碼使用了一個變量t:

function runTowerExperiment(tower, startTime) {
 var t = startTime;
 tower.on("tick", function () {
 ... 使用了變量t的代碼 ...
 });
 ... 更多代碼 ...
 }

到目前為止,一切都很順利?,F(xiàn)在你想添加測量保齡球速度的功能,所以你在回調(diào)函數(shù)內(nèi)部添加了一個簡單的if語句。

function runTowerExperiment(tower, startTime) {
 var t = startTime;
 tower.on("tick", function () {
 ... 使用了變量t的代碼 ...
 if (bowlingBall.altitude() <= 0) {
  var t = readTachymeter();
  ...
 }
 });
 ... 更多代碼 ...
 }

哦,親愛的,之前那段“使用了變量t的代碼”運行良好,現(xiàn)在你無意中添加了第二個變量t,這里的t指向的是一個新的內(nèi)部變量t而不是原來的外部變量。

JavaScript中var聲明的作用域像是Photoshop中的油漆桶工具,從聲明處開始向前后兩個方向擴散,直到觸及函數(shù)邊界才停止擴散。你想啊,這種變量t的作用域甚廣,所以一進入函數(shù)就要馬上將它創(chuàng)建出來。這就是所謂的提升(hoisting)。變量提升就好比是,JS引擎用一個很小的代碼起重機將所有var聲明和function函數(shù)聲明都舉起到函數(shù)內(nèi)的最高處。

現(xiàn)在看來,提升特性自有它的優(yōu)點。如果沒有提升的動作,許多在全局作用域范圍內(nèi)看似合理的完美技術(shù)在立即調(diào)用函數(shù)表達式( IIFE )中通通失效。但在上面演示的這種情況下,提升會引發(fā)令人不愉快的bug:所有使用變量t進行的計算最終的結(jié)果都是NaN。這種問題極難定位,尤其是當你的代碼量遠超上面這個玩具一般的示例,你會發(fā)狂到崩潰。

在原有代碼塊之前添加新的代碼塊會導(dǎo)致詭異的錯誤,這時候我就會想,到底是誰的問題,我的還是系統(tǒng)的?我們可不希望自己搞砸了系統(tǒng)。

而這個問題與接下來這個問題相比就相形見絀了。

問題 #2:循環(huán)內(nèi)變量過度共享

你可以猜一下當執(zhí)行以下這段代碼時會發(fā)生什么,非常簡單:

var messages = ["嗨!", "我是一個web頁面!", "alert()方法非常有趣!"];
 for (var i = 0; i < messages.length; i++) {
 alert(messages[i]);
 }

如果你一直跟隨這個專欄的文章,你知道我喜歡在示例代碼中使用alert()方法??赡苣阋仓?code>alert()不是一個好的API,它是一個同步方法,所以當彈出一個警告對話框時,輸入事件不會觸發(fā),你的JS代碼,包括你的整個UI,直到用戶點擊OK確認之前完全處于暫停狀態(tài)。

請不要輕易使用alert()來實現(xiàn)Web頁面中的功能,我之所以在代碼中使用是因為alert()特性使它變成一個非常有教學(xué)意義的工具。

而且,如果放棄所有笨重的方法和糟糕的行為就可以做出一只會說話的貓,何樂而不為呢?

var messages = ["喵!", "我是一只會說話的貓!", "回調(diào)(callback)非常有趣!"];
 for (var i = 0; i < messages.length; i++) {
 setTimeout(function () {
 cat.say(messages[i]);
 }, i * 1500);
 }

然而一定是哪里不對,這只會說話的貓并沒有按照預(yù)期連說三條消息,它說了三次“undefined”。

你知道問題出在哪里么?

輕松理解Javascript變量的相關(guān)問題

你能看到樹上的毛毛蟲(bug)嗎?(圖片來源: nevil saveri )

事實上,這個問題的答案是,循環(huán)本身及三次timeout回調(diào)均共享唯一的變量i。當循環(huán)結(jié)束執(zhí)行時,i的值為3(因為messages.length的值為3),此時回調(diào)尚未被觸發(fā)。

所以當?shù)谝粋€timeout執(zhí)行時,調(diào)用cat.say(messages[i]) ,此時i的值為3,所以貓咪最終打印出來的是messages[3]的值亦即undefined。

解決這個問題有很多種方法( 這里有一種 ),但是你想,var作用域規(guī)則接連給你添麻煩,如果能在第一時間徹底解決掉這個問題多好??!

let是更完美的var

JavaScript的設(shè)計錯誤(其它語言也有,奈何JavaScript太突出)多半不能被修復(fù)。保持向后兼容性意味著永不改變JS代碼在Web平臺上的行為,即使連標準委員會都無權(quán)要求修復(fù)JavaScript中自動插入分號這種怪異的特性;瀏覽器廠商也從來不會做出突破性的改變,因為如此一來傷害的是他們的忠實用戶。

所以大約十年以前,Brendan Eich決定修復(fù)這個問題,但只有唯一的解決方案。

他添加了一個新的關(guān)鍵詞:let。let與var一樣,也可以用來聲明變量,但它有著更好的作用域規(guī)則。

它看起來是這樣的:

let t = readTachymeter();

或者這樣的:

for (let i = 0; i < messages.length; i++) {
 ...
 }

let與var還是有不同之處的,所以如果你只是在代碼中將var全局搜索替換為let,一些依賴var聲明的獨特特性(可能你不是故意這樣寫)的代碼可能無法正常運行。但對于絕大多數(shù)代碼來說,在ES6的新代碼模式下,你應(yīng)該停止使用var聲明變量,能使用let就用吧!從現(xiàn)在起,請記住這句口號:“l(fā)et是更完美的var”。

那到底let和var有什么不同呢?非常高興你提出這個問題!

這一規(guī)則可以幫助你捕捉bug,除了NaN錯誤以外,每一個異常都會在當前行拋出。

let聲明的變量擁有塊級作用域。也就是說用let聲明的變量的作用域只是外層塊,而不是整個外層函數(shù)。

let聲明仍然保留了提升的特性,但不會盲目提升。在runTowerExperiment這個示例中,通過將var替換為let可以快速修復(fù)問題,如果你處處使用let進行聲明,就不會遇到類似的bug。

let聲明的全局變量不是全局對象的屬性。這就意味著,你不可 以通過window.變量名的方式訪問這些變量。它們只存在于一個不可見的塊的作用域中,這個塊理論上是Web頁面中運行的所有JS代碼的外層塊。

形如for (let x...)的循環(huán)在每次迭代時都為x創(chuàng)建新的綁定。

這是一個非常微妙的區(qū)別,拿我們的會說話的貓的例子來說,如果一個for (let...)循環(huán)執(zhí)行多次并且循環(huán)保持了一個閉包,那么每個閉包將捕捉一個循環(huán)變量的不同值作為副本,而不是所有閉包都捕捉循環(huán)變量的同一個值。

所以在會說話的貓示例中,也可以通過將var替換為let修復(fù)bug。

這種情況適用于現(xiàn)有的三種循環(huán)方式:for-of、for-in、以及傳統(tǒng)的用分號分隔的類C循環(huán)。

let聲明的變量直到控制流到達該變量被定義的代碼行時才會被裝載,所以在到達之前使用該變量會觸發(fā)錯誤。舉個例子:

function update() {
 console.log("當前時間:", t); // 引用錯誤(ReferenceError)
 ...
 let t = readTachymeter()
 }

不可訪問的這段時間變量一直處于作用域中,但是尚未裝載,它們位于臨時死區(qū)(Temporal Dead Zone,簡稱TDZ)中。我一直想用科幻小說來類比這個腦洞大開的行話,但是還沒想好怎么搞。

(脆弱的性能細節(jié):在大多數(shù)情況下,查看代碼就可以區(qū)分聲明是否已經(jīng)執(zhí)行,所以事實上,JavaScript引擎不需要在每次代碼運行時都額外執(zhí)行 一次變量可訪問檢查來確保變量已經(jīng)被初始化。然而在閉包內(nèi)部有時不是透明的,這時JavaScript引擎將會做一個運行時檢查,也就意味著let相對var而言比較慢。)

(脆弱的平行宇宙作用域細節(jié):在一些編程語言中,一個變量的作用域始于聲明之處,而非前后覆蓋整個封閉代碼塊。標準委員會曾考慮過將這種作用域準則賦予let關(guān)鍵詞,但是一旦使用這種準則,原本提前使用變量的語句會導(dǎo)致引用錯誤(ReferenceError),現(xiàn)在該語句不位于let t的聲明作用域中,根本不會引用此處的變量t,而是引用外層作用域的相應(yīng)變量。但是這個方法無法與閉包和函數(shù)提升很好得結(jié)合,所以該提案最終被否決了。)

用let重定義變量會拋出一個語法錯誤(SyntaxError)。

這一條規(guī)則也可以幫助你檢測瑣碎的小問題。誠然,這亦是var與let的不同之處,當你全局搜索var替換為let時也會導(dǎo)致let重定義語法錯誤,因為這一規(guī)則對全局let變量也有效。

如果你的多個腳本中都聲明了相同的全局變量,你最好繼續(xù)用var聲明這些變量。如果你換用了let,后加載的腳本都會執(zhí)行失敗并拋出錯誤。

或者你可以考慮使用ES6內(nèi)建的模塊機制,后面的文章中會詳細講解。

(脆弱的語法細節(jié):let是一個嚴格模式下的保留詞。在非嚴格模式下,出于向后兼容的目的,你仍可以用let命名來聲明變量、函數(shù)和參數(shù),雖然你不會犯傻,但是你確實可以編寫var let = 'q';這樣的代碼!不過let let;無論如何都是非法的。)

在那些不同之外,let和var幾乎很相似了。舉個例子,它們都支持使用逗號分隔聲明多重變量,它們也都支持 解構(gòu) 特性。

注意,class類聲明的行為與var不同而與let一致。如果你加載一段包含同名類的腳本,后定義的類會拋出重定義錯誤。

const

是的,還有一個新的關(guān)鍵詞!

ES6引入的第三個聲明類關(guān)鍵詞與let類似:const。

const聲明的變量與let聲明的變量類似,它們的不同之處在于,const聲明的變量只可以在聲明時賦值,不可隨意修改,否則會導(dǎo)致SyntaxError(語法錯誤)。

const MAX_CAT_SIZE_KG = 3000; // 正確

 MAX_CAT_SIZE_KG = 5000; // 語法錯誤(SyntaxError)
 MAX_CAT_SIZE_KG++; // 雖然換了一種方式,但仍然會導(dǎo)致語法錯誤

當然,規(guī)范設(shè)計的足夠明智,用const聲明變量后必須要賦值,否則也拋出語法錯誤。

const theFairest; // 依然是語法錯誤,你這個倒霉蛋

神秘的代理命名空間

“命名空間是一種絕妙的理念,我們應(yīng)當多加利用!”——Tim Peters,“這是Python之禪”

嵌套作用域是編程語言背后的核心理念之一,這個理念始于大約57年前的 ALGOL,現(xiàn)在回過頭看當時的決定無比正確。

在ES3之前,JavaScript中只有全局作用域和函數(shù)作用域。(讓我們忽略with語句吧。)ES3中引入了try-catch語句,意味著語言中誕生一種新的作用域,只用于catch塊中的異常變量。ES5添加了用于嚴格的eval()方法的作用域。ES6添加了塊作用域,for循環(huán)作用域,新的全局let作用域,模塊作用域,以及求參數(shù)的默認值時使用的附加作用域。

所有自ES3開始添加的其它作用域非常重要,它們的加入使得JavaScript面向過程與面向?qū)ο蟮奶匦赃\行得猶如閉包一樣平穩(wěn)、精準,當然閉包也可以無縫銜接這些作用域?qū)崿F(xiàn)各種功能。或許你在閱讀這篇文章之前從未注意到這些作用域規(guī)則的存在,如果真的這樣,那這門語言就恰如其分地完成了它的本職工作。

我現(xiàn)在可以使用let和const了么?

可以。如果要在Web上使用let和const特性,你需要使用一個諸如 Babel 、 Traceur或 TypeScript 的ES6轉(zhuǎn)譯器。(Babel和Traceur暫不支持臨時死區(qū)特性。)

io.js支持let和const,但是只在嚴格模式下編碼可以使用。Node.js同樣支持,但是需要啟用--harmony選項。

九年前 ,Brendan Eich在Firefox中實現(xiàn)了初版的let關(guān)鍵詞。這個特性在隨后的標準化進程中徹底地被重新設(shè)計了。Shu-yu Guo正在按照新標準對原有實現(xiàn)進行升級,該項目由Jeff Walden和其他人做代碼審查。

總結(jié)

以上就是這篇文章的全部內(nèi)容了,希望本文的內(nèi)容對大家的學(xué)習(xí)或者工作能帶來一定的幫助,如果有疑問大家可以留言交流。

向AI問一下細節(jié)

免責聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進行舉報,并提供相關(guān)證據(jù),一經(jīng)查實,將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI