[JS] 作用域(Scope)與範圍鏈(Scope Chain)
作用域(Scope)
是變數可以被使用的區域
,區分作用域可以避免變數的衝突。
舉例來說,甲、乙班各有一位同學叫小明,分別在兩個班上點名,點到小明都不會有問題,但若是全校廣播找小明,就會出現衝突,不知道要找哪一個小明。
作用域也有上下(內外)層級關係,當自己的作用域不存在特定變數時,可以向上(外)層尋找。
換個例子,雖然自己的班上找不到校長,但可以從自己的學校找到校長。
接著就來看 JavaScript 如何去實現這些功能。
💎 作用域(Scope)
🔸 1、全域作用域(Global Scope)
全域顧名思義就是全部的區域,網頁中的全域物件就是 window
。
在初學 JS 時,會先練習開啟一個空白檔案,並且撰寫變數宣告,這時的變數就屬於全域變數。
不同的宣告方式產生的全域變數也有差異:
- var:宣告後會成為 window 物件底下的一個屬性。
- let、const:宣告後不會放到 window 底下,而是一個區塊作用域。
1
2
3
4
5
6
7
8
9
10
11
12var a = 1;
let b = 2;
const c = 3;
console.log(a); // 1
console.log(window.a); // 1
console.log(b); // 2
console.log(window.b); // undefined
console.log(c); // 3
console.log(window.c); // undefined
🔸 2、函式作用域(Function Scope)
用 var
宣告的變數會在函式 (function)
內區隔,先來看看下面的範例碼:
1
2
3
4
5
6
7
8
9
10
11function f1() {
var a = 1;
f2();
}
function f2() {
console.log(a)
}
f1();
// 執行結果:
// Uncaught ReferenceError: a is not defined
上面的範例可以知道 f2 函式無法取得 f1 函式內宣告的變數。
再來看看另一個經典題目,當 var 遇到 call stack 時出現的詭異狀況。
1
2
3
4
5
6
7
8
9
10for (var i = 1; i < 5; i++) {
setTimeout(() => console.log(i), 500);
}
console.log(i);
// 5
// 5
// 5
// 5
// 5
如果你覺得這個輸出結果非常奇怪,不要忘了 var 的作用域是在函式中,上面這段 for 迴圈並不是函式,所以 var i
是宣告在全域
,迴圈總共執行了四次,但是每次都是覆蓋掉全域中的 i ,最後只有一個全域變數 i
存放數值 5
,接著 setTimeout 裡面的函式執行時因為作用域裡面沒有變數 i,向外找到全域的 i (此時迴圈已經執行完,值是5),才會都印出一樣的結果;除此之外,印出的第一個 5 是來自程式碼最後一行全域的呼叫,也證實了這個變數存在全域。
函式的範圍較大,撰寫時常使用的邏輯判斷都沒辦法區隔作用域,為了區隔作用域,衍生出閉包、立即函式等各種應用技巧,這個問題在 ES6 的版本後才獲得改善。
🔸 3、區塊作用域(Block Scope)
ES6 版本加入的 let
和 const
兩種變數宣告方式,作用域會在大括號 {}
內區隔:
1
2
3
4
5
6
7
8
9
10
11
12
13{
const a = 1;
{
let b = 2;
var c = 3;
}
console.log(a); // 1
console.log(b); // b is not defined
console.log(c); // 3
}
console.log(a); // a is not defined
console.log(b); // b is not defined
console.log(c); // 3
上面的範例可以看到 var
無論在哪個大括號內都可以取用,但 let
和 const
無法在自己存在的大括號外被取用。
常用的 if-else
、for
等方法都會使用大括號,相對於 function
,可以把作用域區分的更細。
前個段落 setTimeout
範例內的 var
改成 let
:
1
2
3
4
5
6
7
8
9for (let i = 1; i < 5; i++) {
setTimeout(() => console.log(i), 500);
}
console.log(i);
// Uncaught ReferenceError: i is not defined
// 1
// 2
// 3
// 4
從結果可以發現,每次的 let i
都是獨立的作用域,所以 setTimeout 實際執行時能夠找到該次迴圈區塊內產生的變數 i,除此之外,全域環境中也不會找到在區塊作用域中的變數 i (出現未定義的錯誤)。
- 直接宣告(不加前綴詞),無論放在
區塊{}
或函式
內都會成為 window 物件底下的一個屬性。1
2
3
4
5
6
7
8dirtyA = 11;
console.log(window.dirtyA); // 11
function dirtyVariable() {
dirtyB = 12;
}
dirtyVariable();
console.log(window.dirtyB); // 12 - 直接宣告在
window
底下的變數可以使用delete
物件方法刪除;而使用var
宣告的全域變數無法使用delete
刪除(介於屬性與非屬性之間)。1
2
3
4
5
6
7x = 1;
delete window.x; // true
console.log(x); // x is not defined
var y = 2;
delete window.y; // false
console.log(y); // 2 - 在 ES5 時可以透過
'use strict'
(嚴格模式)來避免這些不好的操作,ES6 以後一律使用let
和const
。
延伸閱讀:
1、深入JavaScript系列(一):詞法環境
2、Javascript 的嚴格模式 (Strict Mode)