前言
這類問題我被問到不止一次。不得不說 JS 的變數蠻特別的。新手菜鳥會問,連老鳥也都常搞錯。
更甚者⋯⋯近日更是聽到一個自稱有五年經驗的軟體工程師稱:var 宣告的變數是全域變數⋯⋯
我知道我身邊的朋友,也有不少可能不清楚,或是沒探究這麼深入,相關文章有但不多。於是乎,感覺我再想拖延,也應該把這篇文章寫出來。
這篇文章對於你寫更好的 JS 並沒有太多幫助,有許多部分是你平常不太會用到,但卻是非常基礎的概念。儘管不知道,通常按照當前常見規範,程式碼亦不會太糟糕。
這篇文章主要是從一份回覆修改而來。
變數的生存範圍
無關鍵字賦值、var 宣告、let 宣告最大的差別在於生存區域的不同。無關鍵字賦值 意味著全域變數的宣告,當然你在全域範圍使用 var/let 宣告也是全域的。只是無關鍵字可能引發意外的情況,像是你預期變數應該是函數區域的:
上例中全域情況也取得到在 printG 函數裏定義的全域變數。這相當於你顯式定義 g
於全域:
對於 printG 來看,就是去外部找一個 g 來用,找不到就在全域建立一個。
var 宣告
透過 var 宣告的變數,其生存範圍存在於函數內:
所以不同於 g 的情況,v 並不會因爲 printV1 存在於全域。(printV1 裡的 v 並不會影響到全域,同樣,如果你全域有宣告 v 的話,亦不會影響到 printV1 裡的 v)
此外,var 的宣告是屬於函數內的,就算下面這樣寫也沒有問題:
而且你只要在函式內宣告,就不會有問題。所以下面情況也不會報錯:
你甚至可以使用 var 多次宣告:
實際上,這是因為變數提升(Hosting)的關係。在執行函數之前,會優先將var變數放入記憶體。要注意的是:這只是在記憶體有這變數的空間,但尚未初始化。這也是為何會拿到undefined的原因。
JavaScript 僅提升宣告的部分,而不是初始化。如果在使用該變數後才宣告和初始化,那麼該值將是 undefined。
– MDN
let 宣告
使用 let 宣告,和 var 宣告很像,但生存區域更小一些,變數是屬於區塊的(block),同樣不會影響到全域的變數狀態:
不同的是不能像 printV3 在函式任意位置宣告:
而且存在暫時執行死區(TDZ)。不能在宣告前使用,不能多次宣告(內部區塊覆蓋外部區塊可以)。
因爲你可以將內部區塊變數暫時覆蓋外部區這個原因,所以可以用上一些技巧:
上面會迭代印出 table 裡的所有元素,儘管他是二維的,更注意到內 部for 迴圈變數也同樣使用 i,避免了 i、j、k、l,和 table[i][j] 這樣的窘境。這在尤其真的有必要使用大量巢狀迴圈的時候特別有用。但如果真的發生了那種事,或許你應該思考是否真得這樣做,有沒有更好的寫法。
誰會記得自己迴圈變數用到那了呢? i、j、k、l…z ?
只可惜 JavaScript 直譯時的特性,沒辦法同名暫時性覆蓋還使用原本的變數賦值:
如果可以的話,就可以拿原本的變數先做一些變化,還回復到原本的變數值。以下使用 Lua 示例
小結: 生存範圍差異
也就是說,無宣告變數會被視為全域變數,除此之外,寫在全域環境的 var、let、const 變數同樣可以視為全域變數。
但是真正作用上,var 是函數變數,在函數生存範圍都可以存取;let、const 則是區塊變數,只能在宣告後使用,宣告前使用則會報錯。
無宣告變數的特別之處
MDN 這樣寫到:
In the global context, a variable declared using var is added as a non-configurable property of the global object. This means its property descriptor cannot be changed and it cannot be deleted using delete. The corresponding name is also added to a list on the internal [[VarNames]] slot on the global environment record (which forms part of the global lexical environment). The list of names in [[VarNames]] enables the runtime to distinguish between global variables and straightforward properties on the global object.
其中最明顯的差異,除了不管你到哪裡,只要直接賦值就是全域變數外,與 var 最大的差異:就是能不能通過 delet 操作刪除變數。
同樣可以被刪除的,還有物件的屬性:
想轉職網頁開發工程師,該怎麼開始?3 分鐘小測驗,找到你的學習起點
特殊的全域物件 globalThis
這時候我們就需要認識到一個特殊的全域物件:globalThis。在瀏覽器的話是 window;Node.js 的話是 global。
可以發現,其差異在於其屬性描述器的 configurable。透過修該該項,同樣可以讓未宣告變數無法被刪除:
let, const 不會出現在 globalThis
有意思的是 let, const 不會出現在 globalThis。儘管在交互式環境裡你仍然可以存取到他,但他卻存在於另外一塊執行環境。
var 是罪惡嗎?
多數情況下,使用 let 取代 var 絕對沒問題。但去了解兩者的差異也絕對沒有壞處。var 的設計是歷史遺留下來的結果。很多起初作為腳本語言的設計,最初就是為了方便;方便寫腳本、方便語言的實現。
You Don’t Know JS 的作者 Kyle Simpson 認為 var 也是很有用的。
在 ECMAScript(JavaScript)裡,嚴謹/限制程度由高到低爲:const 變數、let 變數、var 變數、全域變數。
以前沒有 const 和 let,現在通常會建議使用 let 而非 var,因爲這樣會在執行時期(Runnint Time)檢查是否有邏輯錯誤。
如果你希望你的程式就算遇到非預期性錯誤,執行起來很怪,也不該報錯。那仍可以考慮使用 var,這也是早期瀏覽器設計的考量。只是這樣的程式並不好除錯與維護。
所以就簡單來說,使用 let 取代 var 通常能夠得到比較健全的程式碼。但是在你了解了var、let 差異後,知道一個是函數變數,另一個是區塊變數後,你可以將變數賦予不同意義。下面我們可以很清楚的知道 studentRecords 和 id、record 變數負責的範圍。
你可以用他來初始化可能出錯的變數:
或是達成Uncle Bob在Clean Code所提到的:「養成將try-catch-finally寫在程式碼開頭的習慣」。這是因為var宣告的是函數變數,在catch區塊依然可以使用。
程式範例
上面範例程式比較的對應表:
- printV1() <-> printL1()
- printV2() <-> printL2()
- printV3() <-> printL3()
最後附上完整程式碼,可以去玩玩看:
小練習
下面還有一個可以想想看的,問過朋友答出正確的不多。先想過後,再執行看看:
參考資料
JavaScript 全端開發課程,16 週進度班帶你半年轉職工程師