Skip to content

作用域 (Scope)

作用域 (Scope) 是指一個變數在程式碼中的有效範圍跟是否可以被存取的區域。簡單來說,就是一個變數可以在哪裡被使用。

理解作用域是掌握 JavaScript 的關鍵一步,它能幫助你避免變數命名衝突和非預期的行為。

為什麼需要作用域?

想像一下在一個大型專案中,如果所有的變數都可以在任何地方被存取和修改,變數名稱可能會互相衝突,一個地方的修改可能會無意中影響到另一個完全不相關的功能,那將會是一場災難。

作用域就是為了解決這個問題,它建立了一個邊界,將變數和函式限制在特定的區域內。

區塊作用域 (Block Scope)

隨著 ES6 的出現,letconst 關鍵字帶來了區塊作用域的概念。區塊是指由大括號 {} 包圍的任何程式碼區域,例如 if 判斷、for 迴圈或單純的 {}

在區塊內使用 letconst 宣告的變數,只能在該區塊內部被存取。這使得程式碼的行為更加可預測,減少了變數污染的可能性。

javascript
if (true) {
  let blockScopedVar = 'I am in a block!';
  const blockScopedConst = 'Me too!';
  console.log(blockScopedVar); // 輸出 'I am in a block!'
}
console.log(blockScopedVar); // ReferenceError: blockScopedVar is not defined
console.log(blockScopedConst); // ReferenceError: blockScopedConst is not defined

for 迴圈也是一個很好的例子:

javascript
for (let i = 0; i < 3; i++) {
  console.log(i); // 這裡的 i 只存在於這個迴圈區塊中
}
console.log(i); // ReferenceError: i is not defined

函式作用域 (Function Scope)

在函式內部使用 var 宣告的變數,其作用域被限制在該函式內部。這就是函式作用域。在函式外部無法存取這些變數。

javascript
function sayHello() {
  var greeting = 'Hello, World!';
  console.log(greeting); // 可以在函式內部存取
}

sayHello(); // 輸出 'Hello, World!'
console.log(greeting); // ReferenceError: greeting is not defined

以上這個範例你可能會想問,如果把 var 換成 letconst 不是也成立嗎?其實不是的,如果將 greeting 改成 let 再將它放到一個區塊作用域裡,那麼 greeting 就會變成區塊作用域的變數,而不是函式作用域的變數:

javascript
function sayHello() {
  if (true) {
    let greeting = 'Hello, World!';
    console.log(greeting); // 可以在區塊內部存取

    var anotherGreeting = 'Hello again!';
  }

  console.log(greeting); // ReferenceError: greeting is not defined

  // 因為 var 宣告的變數是函式作用域,不會因為被 if 區塊包住而報錯
  console.log(anotherGreeting);
}

全域作用域 (Global Scope)

在所有函式和程式碼區塊 ({}) 之外宣告的變數,就擁有全域作用域。這意味著它們可以在程式的任何地方被存取。

javascript
// 這是一個全域變數
const globalMessage = 'Hello, I am global!';

function showMessage() {
  console.log(globalMessage); // 可以在函式內部存取
}

showMessage(); // 輸出 'Hello, I am global!'
console.log(globalMessage); // 也可以在函式外部存取

WARNING

過度使用全域變數是一種不好的實踐。因為它們會汙染全域命名空間,增加命名衝突的風險,並讓程式碼難以追蹤和維護。

巢狀作用域 (Nested Scope) 與作用域鏈 (Scope Chain)

當一個函式或區塊嵌套在另一個函式或區塊中時,就形成了巢狀作用域。JavaScript 引擎在尋找變數時,會有一套規則:

  1. 先在當前作用域尋找。
  2. 如果找不到,就往上一層(外部)作用域尋找。
  3. 重複這個過程,直到找到變數,或到達最外層的全域作用域。
  4. 如果最終在全域作用域還是找不到,就會報錯 ReferenceError

這個由內而外的尋找路徑,就稱為作用域鏈 (Scope Chain)

javascript
const globalVar = 'Global'; // --globalVar有效作用域------------------|
//                                                                  |
function outerFunc() { //                                           | 
  const outerVar = 'Outer'; // --outerVar有效作用域----------|        |
  //                                                       |        |
  function innerFunc() { //                                |        |
    const innerVar = 'Inner'; // --innerVar有效作用域-----|  |        |
    console.log(innerVar);   //                         |  |        |
    console.log(outerVar);   //                         |  |        |
    console.log(globalVar);  //                         |  |        |
  } // -------------------------------------------------|  |        |
    //                                                     |        |
  innerFunc(); //                                          |        |
} // ------------------------------------------------------|        |
//                                                                  |
outerFunc(); //                                                     |
//                                                                  |
// -----------------------------------------------------------------|

補充變數章節沒有提到的 var, let, const 差異

前面在變數章節時,我只對 var, let, 和 const 做了簡單的介紹。現在我們來深入了解它們在作用域方面的差異。

在 JavaScript 中,我們主要使用 var, let, 和 const 這三個關鍵字來宣告變數。它們在作用域、變數提升和可變性方面有著重要的區別。

我們先釐清幾個概念:

  • 暫時性死區 (Temporal Dead Zone, TDZ): 在使用 letconst 宣告變數時,這些變數在宣告之前是無法存取的。這段期間稱為「暫時性死區 (Temporal Dead Zone, TDZ)」,在這段期間內訪問這些變數會導致 ReferenceError。 但是,這個概念只適用於 letconst,而 var 則不受此限制,var 宣告的變數會在一開始給予 undefined
javascript
console.log(x) // 會印出 undefined ✅
console.log(y) // ReferenceError: Cannot access 'y' before initialization
console.log(z) // ReferenceError: Cannot access 'z' before initialization

var x = 10; // 使用 var 宣告的變數
let y = 20; // 使用 let 宣告的變數
const z = 30; // 使用 const 宣告的變數
  • 提升 (Hoisting): 在 JavaScript 中,變數宣告會被提升到其作用域的頂部優先執行。這意味著你可以在宣告之前使用變數,但其值為 undefined。而 let, const, var 都會進行提升,但 letconst 的提升會受到暫時性死區的影響而不能在未宣告時被存取。

  • 可重複宣告 (Redeclaration)var 允許在同一作用域內重複宣告變數,而 letconst 則不允許重複宣告。

javascript
var a = 1;
var a = 2; // 可以重複宣告 ✅

let b = 1;
let b = 2; // SyntaxError: Identifier 'b' has already been declared

const c = 1;
const c = 2; // SyntaxError: Identifier 'c' has already been declared
  • 可變性 (Mutability)varlet 宣告的變數可以被重新賦值,而 const 宣告的變數則不能被重新賦值, 但如果是物件或陣列,其內部屬性或元素是可以被修改的。因為 const 只保證變數的引用不會改變,但如同資料儲存章節所說的,物件屬性的位址跟物件本身的位址是分開的,所以可以修改物件的屬性或陣列的元素,但不能重新賦值整個物件或陣列。
javascript
var x = 10;
x = 20; // 可以重新賦值

let y = 30;
y = 40; // 可以重新賦值

const z = 50;
z = 60; // TypeError: Assignment to constant variable

const obj = { name: 'Aaron' };
obj.name = 'John'; // 可以修改物件的屬性 ✅
obj = { name: 'Doe' }; // TypeError: Assignment to constant variable
變數關鍵字作用域暫時性死區變數提升可重複宣告可重新賦值
var函式作用域
let區塊作用域
const區塊作用域

盡量不要使用 var

在實際開發中,建議全部使用 letconst 來宣告變數,因為它們提供了更嚴格的作用域控制和更清晰的意圖。var 主要用於舊版 JavaScript 或需要向後相容的情況下使用。

而且 var 在網頁的全域作用域中會被加入到 Window 物件中,配合可以重複宣告的特性,極易導致變數污染和命名衝突,因此非常不推薦在新程式碼中使用 var

以下是一個 var 變數導致污染的範例:alertWindow 物件的預設方法,但我們可以用 var 來宣告一個同名的變數,覆蓋掉 Window 物件的 alert 方法,污染全域作用域:

javascript
// 這裡的 alert 是 Window 物件預設,相當於 window.alert('Hello, World!');
alert('Hello, World!');

// 使用 var 宣告同名變數,全域下就等於 window.alert
var alert = 'This is a variable, not a function!'; 

alert('This will not work!'); // TypeError: alert is not a function

// 即使直接用 window 物件也找不回預設的 alert 方法了
window.alert('Hello, World!'); // TypeError: window.alert is not a function

範例練習

  1. 預測以下程式碼的輸出結果,並解釋原因。

    javascript
    let a = 1;
    
    function myFunction() {
      console.log(a);
      let b = 2;
    }
    
    myFunction();
    // console.log(b); // 這一行會輸出什麼?為什麼?
  2. 預測以下程式碼的輸出結果。

    javascript
    let x = 10;
    
    if (true) {
      let x = 20;
      console.log(x); // 這裡會輸出什麼?
    }
    
    console.log(x); // 這裡會輸出什麼?
  3. 以下程式碼有什麼問題?如何修正?

    javascript
    for (var i = 0; i < 5; i++) {
      setTimeout(function() {
        console.log(i);
      }, 1000);
    }
    // 提示:思考 var 的作用域

Wrirten by Aaron Su