前言
這一篇要記錄的觀念是常常會讓人感到困惑,光看名詞就會讓人不知道在講什麼的「範圍鏈 (Scope Chain)」,但實際上如果照著課程走,看到最後會發現,啊原來就只是這樣!所以不要被名詞嚇到了哩。
範圍鏈 Scope Chain
現在我們已經了解「執行堆」、「執行環境」、「變數環境」,讓我們直接看一個範例,會相當眼熟:
1 | function b(){ |
No.2 的時候有提到「外部環境」,還記得「執行堆」的觀念嗎?當我們進入到 b
函式時,會製造一個執行環境,放到執行堆的上面,並把所有已經宣告的變數都放入它的「變數環境」,會在 b
函式的變數環境內尋找 myVar
這個變數,但 b
函式內 myVar
沒被宣告,此時會參考到 b
函式「外部環境」的 myVar
,因此印出來的結果會是 1 。
很混亂嗎?不知道怎麼判斷「外部環境」?
還記得一開始提到的「詞彙環境」嗎,就是代表「程式碼被寫出來的實際位置」,這意味著 JavaScript 引擎會如何處理這些程式在記憶體中的位置、以及與其他程式的連結。
所以回到例子,函式 b
以詞彙上來說,它在全域環境之上,代表 b
函式不在 a
函式內,它與最後一行的 myVar = 1
同層級。
事實上執行環境的實際位置不重要,函式 b
可以在 函式 a
上,或者互換位置,重要的是「執行的順序會決定函式如何被呼叫、執行環境如何被創造」。
JavaScript 相當注重詞彙環境,當你需要某個執行環境內的某個變數時,如果沒辦法找到此變數,它會到外部環境尋找變數,直到執行堆最下方尋找。這是整個範圍鏈到外部環境的過程。
所以如果有許多函式互相呼叫,搜尋範圍鏈會不斷的向下移動,直到全域階層。如果這些函式是互相在內部被定義,也會逐層的在那些外部環境參照尋找,直到找到或者沒找到,這一整個過程就稱為「範圍鏈」
「範圍 (Scope)」代表能夠取用這個變數的地方;「鏈 (Chain)」是外部環境參照的連結;「詞彙」這是實際上程式碼被寫出來的物理位置
如果上面這個例子了解了,我們來點詞彙環境上的改變:
1 | function a(){ |
我們改變了 b
函式的詞彙環境,它在 a
函式內了。
這代表什麼?
這代表現在我們不能在全域的地方呼叫 b
函式,因為全域執行環境會尋找 b
函式,但 b
函式根本不再這個變數環境內, b
函式在 a
函式內。
我們又可以得知一點,當 JavaScript 創造全域執行環境時,它發現 a
函式,但不會看 a
函式內有什麼東西,會直接到 a
函式結束的地方繼續看下去。所以如果直接這樣寫:
1 | b(); |
會得到 b is not defined
的錯誤。
如果呼叫 a 函式呢?
讓我們再來推論一次:
- 當呼叫
a
函式,函式a
的執行、變數環境被創造,此時myVar
的值為 2 - 當程式執行到呼叫
b
函式時,函式b
的執行、變數環境被創造 - 當執行函式
b
內的程式碼console.log(myVar)
,當 JavaScript 往範圍鏈下面找時,它在函式b
內找不到myVar
,便轉向外部環境的函式a
找,因此結果會是myVar = 2
所以讓我們再變化一次:
1 | function a(){ |
延續上面的情境,當 JavaScript 往範圍鏈下面找時,它在函式 b
內找不到 myVar
,便轉向外部環境的函式 a
找,這個時候函式 a
也找不到 myVar
了,但是 a
函式的外部環境參照是全域執行環境,因此找到 myVar = 1
。
後記:
至此,我總算是明白了 JavaScript 在我們沒看到的地方偷偷摸摸的為我們做了什麼事情,如果明白了這些,也對於工作上的除錯有相當大的幫助,因為我們了解了JavaScript 底層做了些什麼,所以當出現一個非預期的值,我們也能快速的除錯。