[JS] Scope & Scope chain

JavaScript 之 Scope 作用域

這裡先針對 JavaScript 的作用域進行說明,但作用域並不是 JS 語言才有,作用域簡單來說就是「一個變數能夠被存取的範圍,如果超出該範圍就不能存取到該變數」,或者也有人說作用域就是一個變數的生存範圍。

在 ES6 之前,唯一能夠產生出作用域的是 function,並以此區分為 local scope (區域作用域) 及 global scope (全域作用域),兩者區別看以下的範例來了解

1
2
3
4
5
6
7
8
9
10
11
12
var a = 'global';

function test() {
var b = 'local';
console.log(a); // global
console.log(b); // local
}

console.log(a); // global
console.log(b); // Uncaught ReferenceError: b is not defined

test();

可以看到在 function test 能夠存取到本地的 b 及外部的 a,但在 test 之外的地方卻存取不到 b

這裡可以看出來在 function 裡面的範圍便是 local scope,而 function 外則為 global scope,在全域作用域宣告的變數稱為 global variable (全域變數) 在代碼中的任何地方都能夠存取到,例如範例中的 a 即為全域變數。

另外要注意的是一個 function 的作用域除了 {...} 之外,還包括了參數的部分也是作用域的範圍。

再看下面這個範例

1
2
3
4
5
if (true) {
var a = 5;
}

console.log(a) //5

可能會錯覺以為結果像 function 一樣存取不到在 if 裡面的變數,但前面說過在 ES6 之前,唯一能夠產生出作用域的是 function,所以 if、for、switch、while 等等是界定不出作用域的。

不過以上為 ES6 之前的情形,在 ES6 之後新增了 block scope 區塊作用域的概念,使得有使用區塊 {...} 語法,也就是 if、for、switch、while 等也能夠界定出作用域,但必須配合 letconst 關鍵字使用,但這部分並非本文主要內容就不多贅述。

Scope Chain 作用域鏈

這裡再看以下代碼

1
2
3
4
5
6
7
8
9
10
11
var a = 10;

function outer() {
var a = 5;
function inner() {
console.log(a) // 5
}
inner();
}

outer();

inner 列印 a 的結果是列印 outer 的 5 ,而不是在全域的 10,會有這樣的結果是因為在 inner 的作用域裡面找不到 a,就會去上一層的作用域,也就是 outer 的作用域尋找,如果還是找不到就再往上一層作用域找,直到找到為止或是最終到全域仍找不到就會拋出 Uncaught ReferenceError: a is not defined 的錯誤。

而像這樣的過程便是 scope chain (作用域鏈),形成 inner scope -> outer function scope -> global scope 這樣的作用域鏈。

Scope 作用域

前面說的 scope 主要是針對 JavaScript 的情況,但 scope 並不限於 JavaScript 語言才有,不同的程式語言可能有不同的作用域,甚至同一語言內也可能存在多種作用域。這裡再說一次 scope 作用域指的是變數或常數能夠被存取到的範圍。

scope 基本可以分為

  • Static Scope (靜態作用域,或者也有人稱其為 Lexical Scope 語彙作用域、詞法作用域)
  • Dynamic Scope (動態作用域)

因為 JavaScript 是採用 Lexical Scope,所以後面再針對靜態作用域說明。

Static Scope 靜態作用域

先來看看下面的代碼

1
2
3
4
5
6
7
8
9
10
11
12
var a = 10;

function print() {
console.log(a); // 10 or 20?
}

function test() {
var a = 20;
print();
}

test();

請問此時的 test() 的結果會是什麼? 正確答案是 10,有人可能認為在 test 裡的 print 找不到 a 便往上一層的 test 作用域找到 a 為 20,如果從前面學的 scope chain 來看似乎沒錯,但結果卻是 10,這就與使用的程式語言是採用何種 scope 有關,在這裡即是 static scope

採用 static scope 的程式在編譯時就能夠確定作用域,也就是能決定在某範圍是否能夠存取某些變數,且該作用域與 function 在哪裡被呼叫無關,而是與在哪裡宣告(定義)有關。

像是 print 定義時,print 的作用域找不到 a 所以往上一層的 global scope 找到 a 為 10,從作用域鏈來看就是 print scope -> global scope,print 對於變數的存取只能在這兩個作用域尋找。

比較下面的代碼

1
2
3
4
5
6
7
8
9
10
function print() {
console.log(b); // 20?
}

function test() {
var b = 20;
print();
}

test();

這次你能看出來會印出什麼? 20?

如果能夠明白上面對於 static scope 的解釋,那麼你應該能知道結果會是顯示 Uncaught ReferenceError: b is not defined 的錯誤。

因為 print 不論是在 print scope,還是 global scope 都找不到變數 b 的存在。

而與 static scope 相反的存在便是 dynamic scope 動態作用域,無法在執行前便知曉函式裡的變數是什麼值,只能在執行時動態的決定,例如使用 dynamic scope 的程式語言執行上面的代碼,結果便會是 20。


參考資料:

  1. 所有的函式都是閉包:談 JS 中的作用域與 Closure
  2. 函式與作用域
  3. 前端中階:JS令人搞不懂的地方-變數的生存範圍(scope)
  4. I Want To Know JS: JavaScript - Lexical Scope
  5. I Want To Know JS: JavaScript - Scope 單位 & 查找
  6. Kuro’s Blog: JavaScript 變數宣告與作用域