Scope 的機制主要像是一組規則定義變數儲存的位置,用於此後需使用此變數時有一個脈絡可循,以生活例子來分享的話,類似於去家樂福,想買沐浴乳就去盥洗用具區,想買蔬菜就去蔬果區,定義區塊分類的規則就是 Scope
JS 是如何剖析 var x = 3 呢? 我們可以拆解為兩步驟來看,
var x-> 詢問 Scope 是否有變數 x 的存在?若有的話進入第二步驟,若沒有的話要求 Scope 內宣告新變數 xx = 2-> 詢問 Scope 是否有變數 x 的存在?若有則 2 存入 x,反之若此Scop e為巢狀,則向外繼續詢問是否有此變數直到最外層 Scope
根據上兩步驟的行為,最後得到結果,而在剖析的過程中,對於 Scope 查找動作又分成兩個小細節,也就是 LHS & RHS
- LHS -> 找出塞入值的變數容器,例如
x = 2為一個 LHS 查詢,塞入值是多少並不是此查詢的重點,重點在於存入 2 的容器為哪個變數 (x) - RHS -> 找出某變數的值,例如
console.log(x)為一個 RHS 查詢,目的在於找出 x 的值以供給 console.log 輸出
提到此兩種查詢行為主要是因為,當今天 Scope 查詢中,若是因為 RHS 無法在 Scopes 中找到相對應的變數的話,所拋出的就是 Reference Error,而若是因為 LHS 查詢失敗的話則是會在 Global Scope 上自動宣告一個 ( 前提為非 strict mode 下)
基本上 Scope 在我們撰寫程式碼的時候已經大致決定 Scope 的範圍,間接也可以理解到所謂的 local variables & global variables 的來源,如以下範例


上圖範例可以看到,當今天從 test 取用到 a & b 時,剖析行為大概為以下,首先在 Global Scope 宣告 test & b,接著執行 test,執行途中需要在 local scope 宣告參數 a 的存在接著把值 3 帶入,當讀取到 b 時,local scope 內並無 b 的宣告資訊於是往外一層去尋找,找到 b 後拿回使用
大家應該會想到,倘若在最外層也找不到 b 呢,試著將 var b = 3 改為 b = 3 測試看看會有什麼結果?同時也試著列出上述範例有幾個 LHS & RHS?
Hoisting
談到 Scope 不免俗談一下 Hoisting 的機制,從上述的我們得知,程式剖析分成宣告的部分 ( var = a ) 以及執行部分 ( a = 2 ),也就是先經過 compiler 剖析好宣告部分再進入實際執行部分,因此宣告都是需要事先完成的,這也是 Hoisting 存在的來源,我們知道變數以及函式宣告是會被拉抬到程式的上端的,如下範例


函式 test 先被 Hoisting 到上方,接著才執行,同理 a 的宣告也被宣告於 Scope 內的最上方,因此我們注意到 Hoisting 是根據 Scope 去拉抬的,再來有個小細節是 Hoisting 是函式優先於變數宣告的

Closure
若試著表達 Closure 的話,我會說大概是函式可以存取 Scope 並取得其中的變數,即時函式執行在其他 Scope 內仍然不影響其存取原本 Scope 的能力,感覺起來有點模糊,小弟實在是無法很明確地去定義 Closure,看範例或許清楚些

上述的範例對大家來說覺得像是 Closure 嗎?嚴格來說或許是吧, testClosure 封閉了一部份的 wrap Scope,但此處用 Scope lookup 來解釋的話或許更是合理一些,對於 Closure 似乎仍是模糊的區塊,在看下面一個範例

由上述看到,我們將 testClosure 當作回傳值丟了出來,而實際上在外部執行的時候卻仍然可以存取到 wrap Scope 裡面的 a ,此時就是所謂的 Closure ,testClosure 紀錄了 wrap Scope 的內容並在其他 Scope 內執行時仍舊無誤儘管 wrap 在執行後已經被回收了
另一個經典的 closure 範例非 for 迴圈莫屬,嘗試解讀一下以下範例的 output 並試著實際推理為什麼會發生這個現象?

結果印出的 6 6 6 6 6,實際上雖然每個 timer 都建立出了自己的 Scope 但實際上每個 Scope 存取到的都是同樣在 global scope 裡面的 i ,而 setTimeOut call back 實際上都是在 for loop 結束後才執行的儘管時差是 0 ,因此迴圈結束後的 i 為 6 全部的 callback 也就自然印出 6 了

當然現在有 es6 let 的宣告可以輕易達到我們原本想要的 1 2 3 4 5,但透過實際的推演明白或許是更能理解 closure & scope 原理的方法,因此大家可以試著不用 let i = 1 的做法去解決此問題,小小提示為利用 closure 讓每個 callback scope 有辦法存取的個別 scope 內的 i 而非指向到 global scope 的 i
明白了 Closure 的機制,我們可以發現其原理很適合作為撰寫 Module 的設計之一,一個好的 Module 就是只給別人看我們讓他們看的,其餘的我們通通藏在 module 裡面

如上例,我們透過了 Closure 的機制存取了建立 Module 時的 addNum 以及內放的 nowSum,即使 nowNum 在函式外被引用仍然可以讀取到內藏的 nowSum,當然也可以儲存不想讓使用者看到的函式以及變數,而只回送開放 API ,透過這樣的設計,Module API 仍舊可以使用內部的函式以及變數同時不透露給使用者
以上就是小弟紐西蘭打工回來後,在家當米蟲時順便複習的觀念,筆記複習內容相對簡要以及鬆散,若有錯誤仍舊請大家留言提點小弟,若是文章提到的小問題無法清楚的推演出來結果的話也歡迎留言,小弟會分享自己的理解過程,最後如果文章有幫助到您一點點的話再麻煩分享或者拍拍手讓小弟感受到一點虛榮感,謝謝大家
Reference -
You don’t know JS by O’Reilly
