[JavaScriptWeird]No.55 變數的生存範圍

前言

本篇會提到一個相當重要的觀念,那就是 Scope 作用域,也就是變數的生存範圍。在 ES6 以前 , JavaScript 的作用域界定都是以函式 function 來劃分,本小節會著重以 ES6 之前的作用域來講解。

變數的生存範圍

1
2
3
4
5
6
function test(){  
var a = 10;
console.log(a);
}

test(); // 10

毫無疑問地會輸出 10 ,但如果我們試著於函式外印出變數 a 的值呢?

1
2
3
4
5
6
7
function test(){ // test scope  
var a = 10;
console.log(a);
}

test(); // 10
console.log(a); // a is not defined

會得到錯誤「a is not defined」,這是因為變數 a 屬於區域變數,在函式外的 console.log(a) 無法取用變數 a

全域變數?區域變數?

如何判斷一個變數到底是屬於全域變數還是區域變數?

  • 沒有被包在函式內的變數就是全域變數
  • 包在函式內的變數就是區域變數

更多的例子:

1
2
3
4
5
6
7
var a = 20;  
function test(){ // test scope
console.log(a); // 20
}

test();
console.log(a); // 20

test 函式內印出的 20 ,因為在 test 的作用域內沒有找到對應的變數 a 所以轉而向上一層尋找變數 a ,因此輸出為 20 。

1
2
3
4
5
6
7
8
var a = 20;  
function test(){ // test scope
var a = 10;
console.log(a); // 10
}

test();
console.log(a); // 20

接續上例,因為在 test 的作用域找到相應的變數 a ,因此就不會繼續往外尋找。由此可知:

  • 當自己的作用域有相應的變數時,就不會繼續往外找了

進階的例子:

1
2
3
4
5
6
7
8
var a = 20;  
function test(){ // test scope
a = 10;
console.log(a); // 10
}

test();
console.log(a); // 10

在這邊要注意的是 test 函式內並不是變數 a ,因為沒有透過 var / let / const 宣告,因此在函式內的 a 是掛載在全域 window 物件下的屬性 a

然而,屬性通常是可以被 delete 刪除的。

但在此情形,如果我們宣告全域變數的話,全域變數會被掛載到 window 物件下成為屬性,而且不可以被刪除

這題的執行流程是這樣的:

  • 全域變數 a 先被賦予初始值 undefined ,此時也成為全域 window 物件下的屬性
  • 對屬性 a 賦值 20
  • 執行 test 函式,對屬性 a 賦值 10 ,第一次印出 a ,結果為 10。
  • 第二次印出時,因為屬性 a 已經被賦值 10 ,因此輸出為 10
  • 注意:屬性是沒有作用域的
    1
    2
    3
    4
    5
    6
    7
    function test(){  
    a = 10;
    console.log(a); // 10
    }

    test();
    console.log(a); // 10

因為屬性是沒有作用域的,這麼寫相當於

  • 宣告全域變數 var a

雖然這麼寫很方便,但是我們應該盡量避免汙染全域變數,應使用 var / let / const 宣告變數。

需要更多例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var a = 'global';  
function test(){
var a = 'test scope a';
var b= 'test scope b';
console.log(a,b); // test scope a test scope b
function inner(){
var b = 'inner scope b';
console.log(a,b); // test scope a inner scope b
}
inner();
}

test();
console.log(a); // global

一個較貼近實務的範例可能會長得像這樣,雖然比較結構複雜,但是只要掌握一個原則:

  • 作用域是以函式來劃分
  • 當變數在自身作用域內找不到時,會往外一層尋找,最後找到全域作用域

範圍鏈 (Scope Chain)

延續上面的範例,如果我們把某一行註解掉:

1
2
3
4
5
6
7
8
9
10
11
12
13
var a = 'global';  
function test(){
// var a = 'test scope a';
var b= 'test scope b';
console.log(a,b); // test scope a test scope b
function inner(){
var b = 'inner scope b';
console.log(a,b); // test scope a inner scope b
}
inner();
}
test();
console.log(a); // global

我們把 test 函式內的 a 註解,此時 test 函式與 inner 函式的作用域內均找不到變數 a ,因此最終會在全域作用域內找到全域變數 a

其尋找路徑如下:

  • test scope -> global scope
  • inner scope -> test scope-> global scope

像這樣逐層往外尋找某變數的方式,被稱為範圍鏈 (Scope Chain)

範圍鏈是如何決定的

範圍鏈的判斷是以詞彙環境來決定, 指的是程式碼在整個程式中的「實際位置」,像是下面的例子:

1
2
3
4
5
6
7
8
9
var a = 'global';  
function change(){
var a = 10;
test();
}
function test(){
console.log(a); // global
}
test();

像這樣,雖然看起來我們在 change 函式內宣告了變數 a 也在裡面呼叫了函式 test ,但是實際上, test 函式的詞彙環境並沒有包在 change 函式內,因此它的範圍鏈仍然是這樣的:

  • test scope -> global scope

另外在 change 函式內呼叫函式 test 絕對不會是這個樣子

1
2
3
4
5
6
7
8
9
10
11
12
var a = 'global';  
function change(){
var a = 10;
test(); // 呼叫後自動產生以下內容
function test(){
console.log(a);
}
}
function test(){
console.log(a);
}
test();

如果想要建立出如下的範圍鏈:

  • test scope -> change scope -> global scope
    1
    2
    3
    4
    5
    6
    7
    8
    var a = 'global';  
    function change(){
    function test(){
    console.log(a);
    }
    test();
    }
    change();

則應該改變 test 函式的詞彙環境。

因為範圍鏈是以詞彙環境函式被宣告在哪裡來決定的,並不會因為在哪裡被呼叫而改變範圍鏈。

以上就是 ES6 之前對於 scope 的概念,下一篇將記錄 ES6 之後對於 scope 有什麼新觀念要了解。

心得

本篇用到了相當多的範例來解釋不同作用域下輸出的值會是多少,比起「奇怪部分」來說,「奇怪部分」在這邊的解釋是使用一張大圖,搭配一個例子來解釋整個作用域與範圍鏈,而這邊是採用類似這樣的方式。

  • test scope -> change scope -> global scope

這麼做還蠻有用的,比起只看圖而言,透過這樣子寫出來也容易加深自己的印象!

0%