[JS] Scope 作用域

Lochs
Take a day off
Published in
9 min readMay 2, 2020

關於作用域,可以理解為一個 “變數” 與其 “被賦予的值” 所『成立』的範圍。

在 JS 中,當變數在執行環境中被宣告的那一刻起,它就開始存在了。但它與它所綁定的值可以影響的區域到哪裡呢?關於這個問題,就必須討論到作用域。

SCOPE

JavaScript 的作用域可以分為以下三個層級:

Global Level Scope

Function Level Scope (Local Scope)

Block Level Scope (ES6)

Global Level Scope

JavaScript 代碼在編譯階段的最初,會產生一個全域執行環境 (Global Execution Context)。而在全域的執行環境中,會存在一個全域的變數物件 (Global Variable Object)。只要是在全域環境中被宣告的變數 (或者函式),就會被存放在這個 Global Variable Object 內,並且可在整個程式的任何地方被存取。這種變數,我們可以說它的作用域是全域 Global Level Scope,也就是所謂的全域變數 (Global Variable)。

因此,全域變數不管是在函式內,還是函式外被使用,都是有效力的。

const number = 100  // 全域變數const func = () => {
console.log(number) // 100
}
console.log(number) // 100

像這個變數 number,在函式內外都可以被使用。待會作用域鏈的說明中也會再提到。

Function Level Scope

而一個變數如果是在函式內 (函式的 {} 內) 被宣告。他的影響就只限於這個函式最外層的 {} 中。這種變數出了 {} 就完全沒用了 (記憶體會被回收)。想要使用它會出現 ReferenceError。而我們會把這種變數稱之為區域變數 (Local Variable)。

const func = (() => {
const number = 100 // 區域變數
console.log(number) // 100
})()
console.log(number) // ReferenceError: a is not defined (外面抓不到裡面的變數啦!)

# 在 function 內不宣告,直接賦值一個變數可以讓這個變數變成全域變數 (好饒舌…)。但因為不建議這樣搞,所以這邊不討論。

另外,在函式外如果有著一個和函式內相同名稱的全域變數,會優先使用函式內的區域變數。如下:

const number = 999 // 全數變數 numberconst func = (() => {
const number = 100 // 區域變數 number
console.log(number) // 100
})()
console.log(number) // 因為是在全域環境調用 number ,這個 number 會是全域變數的 number = 999。

Block Level Scope (ES6)

塊級作用域 (Block Level Scope) 是一種更小的作用域。只存在於 {} 中。最常出現在 Function Scope 中的 {} 中(像是 if、for 等語法)。聰明的你也許會想,是不是也可以將 Function Scope 看成是 Block Scope 的一種?是的,雖然有點不同,但要這麼想也不是不行。更何況,在 ES6 之前,是沒有塊級作用域這種概念的。

在 ES6 之前,我們宣告變數只能使用 var 來宣告,而使用 var 宣告變數會有不少缺點,也無法形成 Block Scope (只有 Function Level Scope 和 Global Level Scope)。以至於 ES6 推出了另外兩個宣告變數的方式:const 和 let。

在函式中用 const 或 let 宣告變數,都會立即讓這個變數在函式中擁有 Block Level Scope。

let func = (() => {
let number = 999; {
console.log(number); // 999
}
console.log(number); // 999
})()
console.log(number); // ReferenceError: number is not defined

上面這段程式碼,使用了 let 宣告變數 number;所以 number 在 {} 內擁有塊狀作用域。number 只成立於宣告它時它位於的 {} 內,以及它的子 {} 內。

let func = (() => {
let number = 999;
let number = 9999;
console.log(number) // SyntaxError: Identifier 'number' has already been declared
})()

let 或 const 在同一個執行環境中不能重複宣告變數

用 var 關鍵字宣告變數的缺點這邊稍微簡述,就不深入說明了。也因為這些缺點,所以在 ES6 後,宣告變數還是建議只使用 const 或者 let 哦!

var 的缺點:

允許重複宣告

不支援區塊作用域 (Block Scope)

不支援常數 (Constant) 特性

另外 let 跟 const 的不同簡單說就是,用 let 宣告變數可以不斷重新賦值;用 const 宣告變數並賦值後,就無法更動了。

所以作用域應該沒問題了!反正變數作用到哪裡,在它的作用域之外就完全沒作用。但是…最一開始的例子…

const number = 100  // 全域變數
const func = () => {
console.log(number) // 100
}
console.log(number) // 100

像這樣在 func 的執行環境內找不到變數 number。是會往外層找去的。剛剛有提到,外層無法使用內層的變數;但像上面的程式碼,內層卻可以使用外層的變數。這時我們就可以往下繼續提到作用域鏈 (scope chain)

Static Scope / Lexical Scope

等等…不是要說作用域鏈 (scope chain)嗎?Static Scope 靜態作用域 (也可以稱為 Lexical Scope 語彙作用域) 是什麼東西啊?嗯…我是覺得這邊可以先提一下對於作用域應該會有更深刻的理解。來看一下以下程式碼…

var thisIs = 'global';let func1 = () => {
console.log(thisIs)
}
let func2 = () => {
var thisIs = 'local';
func1();
}
func1() // 'global'
func2() // 'global'

驚不驚喜?意不意外?func2() 印出 global 很合理,但 func1 竟然也印出 global 耶!其實只要記得以下簡單的說明:

某程式語言在 func1 的地方會印出 global ,這種程式語言採用的就是靜態作用域 (Static Scope),例如:c / java / javascript ; 反之如果在 func1 的地方會印出 local的,這種程式語言採用的就是動態作用域 (Dynamic Scope),例如:perl。

靜態作用域跟動態作用域最大的區別就是,靜態作用域函式內的變數是在這個函式被宣告時 (代碼被編譯時) 就已經設定好的,也就是 early binding。所以不管這個函式在哪被呼叫,它內部的變數位置早就被決定了,並不會因為被呼叫的位置而改變。

而動態作用域,則是取決於函式被執行時代碼的狀態,來決定函式內的變數。也就是 late binding。

總之對於 Javascript,只要暸解它是採用靜態作用域 (Static Scope) 或是所謂的語彙作用域 (Lexical Scope) 就可以了。

Scope Chain

好了!有了以上的知識後,要理解作用域鏈 (Scope Chain) 就挺容易了。

let number = 1000000;let outerFunc = () => {
console.log(number)
let innerFunc = () => {
console.log(number)
}
innerFunc()
}
outerFunc()

上面的程式碼,雖然在函式內的執行環境中找不到 number 這個變數。 但內部的函式會一層層地往外部的執行環境中找去。直到找到全域的執行環境為止。如果還是找不到,就會拋出錯誤。在 innerFunc 的執行環境內找不到 number 這個變數,就往 outerFunc 的執行環境找去;一樣找不到?再往全域的執行環境找…這樣的行為 — 由內到外的這條找尋鏈,我們就稱呼它為作用域鏈

另外,對於 innerFunc 來說,它的執行環境內並沒有 number 這個變數卻可以存取它。 number 就是 innerFunc 這個函式的自由變數 (Free Variable)。

當然,number 也是 outerFunc 這個函式的自由變數。(因為一樣,在它的執行環境內並沒有 number 這個變數卻可以存取它。)

接下來還可以繼續探討 JS 中的閉包 Closure,還有作用域鏈到底是怎麼進行連接的。

--

--