六角學院 JS 核心筆記 (三)【執行環境與作用域】- 語法作用域 (Lexical Scope)

簡介

語法作用域 (Lexical scope) 的類別則是在探討有相同的變數名稱時,該變數需要參照哪一個值,也就是變數和值之間的聯繫 (binding)。

静態作用域 (Static Scoping)

靜態作用域又叫做詞法作用域 (lexical scoping),採用詞法作用域的變數叫詞法變數。編譯器或直譯器在讀取詞法變數的值時,會檢查包住該變數的函式被定義時的文字環境。也就是函式寫好時,作用域就被定義好了,開發者可以從程式碼中直接觀察到變數的作用域;不需要考慮「該函式是否被其他函式呼叫?」、「該變數是否需要參照其他函式中的同名變數?」等這類和 call stack 有關的問題。

In this scoping a variable always refers to its top level environment. This is a property of the program text and unrelated to the run time call stack.
Static scoping also makes it much easier to make a modular code as programmer can figure out the scope just by looking at the code. In contrast, dynamic scope requires the programmer to anticipate all possible dynamic contexts.

使用靜態作用域的語言,函式在執行之前 (定義的時候) 就已經確定了它的作用域,確定了它能使用的變數。現代大部分的語言都是屬於靜態作用域,例:C, C++, Java 和 JavaScript。

以 C 為例

下面的例子中,f() 要回傳 x 時,因為 f() 內部沒有宣告 x,因此向定義 f() 的外部查找,也就是全域 global environment。因此,f() 只會回傳全域變數 x 的值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// A C program to demonstrate static scoping. 
#include<stdio.h>
int x = 10; // 全域變數 x

// Called by g()
int f() {
return x;
}

// g() has its own variable named as x and calls f()
int g() {
int x = 20;
return f();
}

int main() {
printf("%d", g()); // // 呼叫函式,輸出 10
printf("\n");
return 0;
}

而被函式聲明的一個變數,在該函式內層的其他所有函式都能被使用 (但是使用時,如果不是全域變數,記得傳遞進去)。除非該變數被另一個宣告於內部的同名變數所覆蓋。例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#include<stdio.h> 
int x = 10; // 全域變數 x

int f() {
return x;
}

void h(int x) {
printf("這是 h 中的:%d\n", x);
}
int g() { // g 和 x = 10 定義在同一層

printf("這是 g 中的:%d\n", x);
int x = 20; // 宣告另一個同名變數,覆蓋「全域變數 x」
printf("這是第二次 g 中的:%d\n", x);

h(x);

return f();
}

int main() {
printf("這是 f 回傳的:%d\n", g()); // 呼叫函式
return 0;
}
1
2
3
4
// 這是 g 中的:10
// 這是第二次 g 中的:20
// 這是 h 中的:20
// 這是 f 回傳的:10

以 JS 為例

執行第 11 行 fn2() 時,內部會執行 fn1(),因為並沒有傳值 (value) 進入 fn1(),而且 fn1() 內部也沒有宣告 value,因此當需要印出變數的內容時,第 4 行的 value 會直接向定義 fn1() 的上層環境或上層函式查找,於是找到第 1 行 var value = 10

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

function fn1() {
console.log(value); // 輸出 10
}
function fn2() {
var value = 20;
fn1();
}

fn2(); // 呼叫函式

動態作用域 (Dynamic Scoping)

而動態作用域的語言則是當函式執行時,參照被呼叫時最近的變數,因此需要去考慮 call stack。 例如以下的 pseudo code,因為呼叫 Fn1() 時,離第 8 行最近的是第 7 行 int value = 20

1
2
3
4
5
6
7
8
9
10
11
12
13
int value = 10;

void Fn1() {
Console.WriteLine(value); // 輸出 20
}
void Fn2() {
int value = 20;
Fn1();
}

void Main() {
Fn2(); // 呼叫函式
}

可能混淆的名詞

最初一開始我把「靜態作用域 / 動態作用域」和「靜態語言 / 動態語言」搞混了,如果有跟我一樣困擾的人可以參考我的另一篇筆記:靜態語言 / 動態語言、強型別 / 弱型別、静態作用域 / 動態作用域

參考資料:
六角學院:JavaScript 核心篇 - 邁向達人之路
GeeksforGeeks:Static and Dynamic Scoping