前言
本篇會提到一個相當重要的觀念,那就是 Scope 作用域,也就是變數的生存範圍。在 ES6 以前 , JavaScript 的作用域界定都是以函式 function 來劃分,本小節會著重以 ES6 之前的作用域來講解。
變數的生存範圍
1 | function test(){ |
毫無疑問地會輸出 10 ,但如果我們試著於函式外印出變數 a
的值呢?
1 | function test(){ // test scope |
會得到錯誤「a is not defined
」,這是因為變數 a
屬於區域變數,在函式外的 console.log(a)
無法取用變數 a
。
全域變數?區域變數?
如何判斷一個變數到底是屬於全域變數還是區域變數?
- 沒有被包在函式內的變數就是全域變數
- 包在函式內的變數就是區域變數
更多的例子:
1 | var a = 20; |
test
函式內印出的 20 ,因為在 test
的作用域內沒有找到對應的變數 a
所以轉而向上一層尋找變數 a
,因此輸出為 20 。
1 | var a = 20; |
接續上例,因為在 test
的作用域找到相應的變數 a
,因此就不會繼續往外尋找。由此可知:
- 當自己的作用域有相應的變數時,就不會繼續往外找了
進階的例子:
1 | var a = 20; |
在這邊要注意的是 test
函式內並不是變數 a
,因為沒有透過 var
/ let
/ const
宣告,因此在函式內的 a
是掛載在全域 window
物件下的屬性 a
。
然而,屬性通常是可以被 delete
刪除的。
但在此情形,如果我們宣告全域變數的話,全域變數會被掛載到 window
物件下成為屬性,而且不可以被刪除。
這題的執行流程是這樣的:
- 全域變數
a
先被賦予初始值undefined
,此時也成為全域window
物件下的屬性 - 對屬性
a
賦值 20 - 執行
test
函式,對屬性a
賦值 10 ,第一次印出 a ,結果為 10。 - 第二次印出時,因為屬性
a
已經被賦值 10 ,因此輸出為 10 - 注意:屬性是沒有作用域的
1
2
3
4
5
6
7function test(){
a = 10;
console.log(a); // 10
}
test();
console.log(a); // 10
因為屬性是沒有作用域的,這麼寫相當於
- 宣告全域變數
var a
雖然這麼寫很方便,但是我們應該盡量避免汙染全域變數,應使用 var
/ let
/ const
宣告變數。
需要更多例子
1 | var a = 'global'; |
一個較貼近實務的範例可能會長得像這樣,雖然比較結構複雜,但是只要掌握一個原則:
- 作用域是以函式來劃分
- 當變數在自身作用域內找不到時,會往外一層尋找,最後找到全域作用域
範圍鏈 (Scope Chain)
延續上面的範例,如果我們把某一行註解掉:
1 | var a = 'global'; |
我們把 test
函式內的 a
註解,此時 test
函式與 inner
函式的作用域內均找不到變數 a
,因此最終會在全域作用域內找到全域變數 a
。
其尋找路徑如下:
- test scope -> global scope
- inner scope -> test scope-> global scope
像這樣逐層往外尋找某變數的方式,被稱為範圍鏈 (Scope Chain)
範圍鏈是如何決定的
範圍鏈的判斷是以詞彙環境來決定, 指的是程式碼在整個程式中的「實際位置」,像是下面的例子:
1 | var a = 'global'; |
像這樣,雖然看起來我們在 change 函式內宣告了變數 a 也在裡面呼叫了函式 test ,但是實際上, test 函式的詞彙環境並沒有包在 change 函式內,因此它的範圍鏈仍然是這樣的:
- test scope -> global scope
另外在 change 函式內呼叫函式 test 絕對不會是這個樣子:
1 | var a = 'global'; |
如果想要建立出如下的範圍鏈:
- test scope -> change scope -> global scope
1
2
3
4
5
6
7
8var a = 'global';
function change(){
function test(){
console.log(a);
}
test();
}
change();
則應該改變 test
函式的詞彙環境。
因為範圍鏈是以詞彙環境、函式被宣告在哪裡來決定的,並不會因為在哪裡被呼叫而改變範圍鏈。
以上就是 ES6 之前對於 scope 的概念,下一篇將記錄 ES6 之後對於 scope 有什麼新觀念要了解。
心得
本篇用到了相當多的範例來解釋不同作用域下輸出的值會是多少,比起「奇怪部分」來說,「奇怪部分」在這邊的解釋是使用一張大圖,搭配一個例子來解釋整個作用域與範圍鏈,而這邊是採用類似這樣的方式。
- test scope -> change scope -> global scope
這麼做還蠻有用的,比起只看圖而言,透過這樣子寫出來也容易加深自己的印象!