[JavaScript] Scope 作用域是什麼?
Cathy P

Scope 是什麼?

Scope 中文翻作「作用域/範疇…」代表了變數可以被取用的範圍,如果嘗試在 scope 外存取變數,就會發生錯誤。

下面的範例就是因為在 localText 的 scope 外存取 localText ,所以發生了錯誤。

1
2
3
4
function hello() {
var localText = "Hello Function!";
}
console.log(localText); // Uncaught ReferenceError: localText is not defined

那麼 Scope 的範圍有哪幾種呢?

  1. Global Scope:在任何地方都能存取此變數。
  2. Function Scope:在 function 內部能存取此變數。
  3. Block Scope:在 block{}內部能存取此變數。
  4. Module Scope:

向外找的特性

知道變數的 Scope 範圍,有助於幫我們判斷現在存取的變數究竟是誰。

在區域範圍內時,可以讀到自己區域的變數以及外面區域的變數,但全域的地方無法讀到區域的變數。

為了幫助理解以下的程式,在這邊先知道用 var 宣告的變數,若在 function 外宣告,作用域為 global;在 function 內宣告,作用域在 function 內。
後面會有更詳細的介紹。

看一下這個範例:

1
2
3
4
5
6
7
var globalVariable = "G"; // 作用域為全域
function test() {
var localVariable = "L"; // 作用域在 test() 內
console.log(globalVariable); // "G"
}
test();
console.log(localVariable); // Uncaught ReferenceError: localVariable is not defined

test() 內可以讀到外面的 globalVariable ,但 global 區域沒辦法讀到 test() 內的變數,因此會報錯。

所以簡單來說在存取變數的時候,會先在區域內找這個變數,如果找不到再向外面一層找。


如果變數有重名的情況,會先在小區域找,找不到的話才會再往外找:

1
2
3
4
5
6
7
var username = "Cathy"; // 作用域為全域
function callName() {
var username = "Winni"; // 作用域在 callName() 裡面
console.log(username); // "Winni"
}
callName();
console.log(username); // "Cathy"

第一個 console.logcallName() 內找得到 username 這個變數,所以印出 "Winni"

第二個 console.logcallName() 外面,不會往 callName 內讀取變數,所以他印出外面的全域變數 "Cathy"


如果有多個 function 也是一樣,會先在小區域找,找不到的話再往外找:

1
2
3
4
5
6
7
8
9
var username = "Cathy"; // 作用域為全域
function callName() {
var username = "Winni"; // 作用域在 callName() 裡面
function test() {
console.log(username); // "Winni";
}
test();
}
callName();

這個範例,test() 裡面找不到 username ,所以向外一層在 callName() 中找到了,印出 Winni


接下來出一個小題目,讓大家自己判斷一下 console.log 會印出什麼呢?

1
2
3
4
5
6
7
8
9
10
var username = "Cathy"; // 作用域為全域
function callName() {
var username = "Winni"; // 作用域在 callName() 裡面
function test() {
var username = "Joy"; // 作用域在 test() 裡面
console.log(username);
}
test();
}
callName();

.
.
.
.
.
答案是:"Joy"

因為 test() function 本身就找得到 username,所以印出來的就是宣告在 test() 裡的 username,其值為 "Joy"


第二題小測驗

1
2
3
4
5
6
7
var username = "Cathy"; // 作用域為全域
function callName() {
var username = "Winni"; // 作用域在 callName() 裡面
console.log(username);
}
username = "Frank";
callName();

.
.
.
.
.
答案是:"Winni"

第六行修改的 username 為全域變數的 username,而非宣告在 callName() 裡的 username,所以 callName() 裡的 username 值不會改變。

console.logcall() 中找得到 username ,所以會印出 Winni


第三題小測驗

1
2
3
4
5
6
var username = "Cathy"; // 作用域為全域
function callName() {
username = "Winni";
}
callName();
console.log(username);

.
.
.
.
.
答案是:Winni

callName() 中找不到 username ,所以他向外一層找到了全域作用域的 username ,並將其值修改成 Winni,因此 console.log 印出來的值為被修改過的 Winni

從上面幾個範例,只要我們知道變數的作用域是哪裡,就能判斷存取的變數究竟是哪個變數了。

但是,我們要怎麼知道變數的作用域是哪裡呢?

用 var 宣告變數

如果在 function 外用 var 宣告的變數,那麼該變數就是的 scope 是 global,在任何地方都能存取。

1
2
3
4
5
6
7
8
9
10
var group = "GFRIEND"; // 作用域為全域
console.log(group); // "GFRIEND"

if (group) {
console.log(group); // "GFRIEND"
}

function getName() {
console.log(group); // "GFRIEND"
}

如果在 function 內用 var 宣告的變數,那麼該變數的 scope 是 function,在 function 內部才能存取。

1
2
3
4
5
6
function getName() {
var group = "VIVIZ";
console.log(group); // "VIVIZ"
}
getName();
console.log(group); // Uncaught ReferenceError: group is not defined

第二個 console.log 因為無法讀取到 function 內部宣告的變數,所以會報錯。

Function Scope 的問題

問題一:範圍太大,可能無意間造成覆蓋。

1
2
3
4
5
6
7
8
9
10
function test() {
var count = 20;
if (count > 0) {
var count = 0;
console.log(count); // 0
}
console.log(count); // 0
}

test();

雖然上面的程式碼看起來是宣告了兩個變數,但因為是在同一個 function scope 內宣告兩次 count 變數,因此兩個變數實際上是同一個變數,在修改 if 裡面的 count 值時,外面的 count 同時也被修改了。

這顯然不太符合我們的預期。

問題二:

1
2
3
4
5
for (var i = 0; i < 5; i++) {
setTimeout(() => {
console.log(i);
}, 5000);
}

正常我們應該預期結果要印出 1,2,3,4,5,但實際上會印出 5,5,5,5,5。

因為我們以為,程式是這樣運作的:

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
for (...){
setTimeout(() => {
console.log(1);
}, 5000);
}
for (...){
setTimeout(() => {
console.log(2);
}, 5000);
}
for (...){
setTimeout(() => {
console.log(3);
}, 5000);
}
for (...){
setTimeout(() => {
console.log(4);
}, 5000);
}
for (...){
setTimeout(() => {
console.log(5);
}, 5000);
}

但實際上卻是:

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
for (...){
setTimeout(() => {
console.log(i);
}, 5000);
}
for (...){
setTimeout(() => {
console.log(i);
}, 5000);
}
for (...){
setTimeout(() => {
console.log(i);
}, 5000);
}
for (...){
setTimeout(() => {
console.log(i);
}, 5000);
}
for (...){
setTimeout(() => {
console.log(i);
}, 5000);
}

因為 i 這個變數不在 function 裡面宣告,所以是 global 範圍的變數。當 setTimeout() function 倒數完一秒要印出 i 時, 是迴圈跑完的時候, i 的值已經變成了 5

諸如此類的問題,讓我們使用起來有些不便,到了 ES6 之後,新增了 block scope 的概念。

用 let 和 const 宣告的變數

在 ES6 版本,有了新的宣告變數的方式: letconst,使用這兩種方式會以 block 來區分作用域的範圍,也就是用 {} 來區分的意思。

在 block 之外使用 letconst 宣告的變數,如同在 function 外使用 var 宣告的變數一樣,都是 global scope 的,在任何地方都能使用。

1
2
3
4
5
6
7
8
let count = 1;
if (count > 0) {
console.log(count); // 1
}
function test() {
console.log(count);
}
test(); // 1

在 block 內用 letconst 宣告的變數,scope 為 block 內部。

修改 {} 內的 name ,並不會影響到外面的 name

1
2
3
4
5
6
let name = "John";
{
let name = "Kevin";
console.log(name); // "Aden"
}
console.log(name); // "Kevin"

更常的情況,我們會應用在 if 判斷式或是 for 等迴圈裡。

判斷是和迴圈的括號也算在 block 的範圍內,像是 for(let i=0; i<10; i++){}i

block 內用 let 宣告的變數,不會影響到 block 外的變數。

1
2
3
4
5
6
let count = 1;
if (count > 0) {
let count = 0;
console.log(count); // 0
}
console.log(count); // 1

在同一個 block scope 用 letconst 重複宣告變數的話,會發生錯誤:

1
2
3
4
5
if (count > 0) {
let count = 0;
console.log(count); // 0
let count = 1; // Uncaught SyntaxError: Identifier 'count' has already been declared
}

如果是在迴圈裡用 letconst 宣告變數的話,每一次迴圈的是一次新的 block scope。

所以在迴圈裡宣告變數,並不會算重複宣告變數:

1
2
3
4
for (var i = 0; i < 5; i++) {
let count = 0;
console.log(count);
}

會印出五次 0


也可以解決上面用 var 宣告的問題二,原本的程式是這樣:

1
2
3
4
5
for (var i = 0; i < 2; i++) {
setTimeout(() => {
console.log(i);
}, 5000);
}

會印出 2,2

但我們改成用 let 宣告的話,就可以正常印出 0, 1

1
2
3
4
5
for (let i = 0; i < 2; i++) {
setTimeout(() => {
console.log(i);
}, 5000);
}

這個例子我其實理解了很久才懂,因為每一次迴圈都是新的 block scope,所以 i 的值並不會被修改到。

迴圈跑第一次的時候程式看起來像是這樣:

1
2
3
4
5
6
7
for (...) {
let i = 0; // 第一次迴圈,所以 i 拿到 for 條件裡設定的 0 。
setTimeout(() => {
console.log(i);
}, 5000);
i++;
}

第二次則會變成:

1
2
3
4
5
6
for (...) {
let i = 1; // 第二次迴圈,所以依據條件上一次迴圈最後 i 的值 。
setTimeout(() => {
console.log(i);
}, 5000);
}

兩次迴圈的 i,是獨立互不相干擾的變數,所以就能正常的印出 1, 2

靜態作用域

在討論靜態作用域之前,大家先來思考一下,在哪裡呼叫 function,會不會影響到變數的值?

我們看下面這個範例:

1
2
3
4
5
6
7
8
9
var name = "Cathy";
function callName {
console.log(name);
}
function test(){
var name = "Winni";
callName();
}
test();

我們在 callName() 裡面找不到 name 變數,所以要向外一層找找看有沒有 name

那麼這個外面一層,究竟是定義 function 時的外面 —— 也就是 global 區域,還是呼叫 function 時的外面 —— 也就是 test() 區域呢?

答案是 global 區域,所以 console.log(name) 印出來應為 "Cathy"

因為在撰寫時變數的作用域就已經確定好了,所以稱之為「靜態作用域」。

總結

關於 scope 這個主題,原本處於一個大概知道在做什麼的了解,但仔細要寫起來,也發現了一些原本沒有注意到的細節。像是「靜態作用域」就是在寫這篇文章時,第一次學到的知識。

這篇文章來來回回改了很多遍,因為總是會有沒有先講A介紹B的時候會不清楚,但沒有先講B介紹A也會有點模糊等雞生蛋蛋生雞的問題,希望我最後呈現的內容能讓人好懂。