作用域 (Scope)
作用域 (Scope) 是指一個變數在程式碼中的有效範圍跟是否可以被存取的區域。簡單來說,就是一個變數可以在哪裡被使用。
理解作用域是掌握 JavaScript 的關鍵一步,它能幫助你避免變數命名衝突和非預期的行為。
為什麼需要作用域?
想像一下在一個大型專案中,如果所有的變數都可以在任何地方被存取和修改,變數名稱可能會互相衝突,一個地方的修改可能會無意中影響到另一個完全不相關的功能,那將會是一場災難。
作用域就是為了解決這個問題,它建立了一個邊界,將變數和函式限制在特定的區域內。
區塊作用域 (Block Scope)
隨著 ES6 的出現,let 和 const 關鍵字帶來了區塊作用域的概念。區塊是指由大括號 {} 包圍的任何程式碼區域,例如 if 判斷、for 迴圈或單純的 {}。
在區塊內使用 let 或 const 宣告的變數,只能在該區塊內部被存取。這使得程式碼的行為更加可預測,減少了變數污染的可能性。
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 definedfor 迴圈也是一個很好的例子:
for (let i = 0; i < 3; i++) {
console.log(i); // 這裡的 i 只存在於這個迴圈區塊中
}
console.log(i); // ReferenceError: i is not defined函式作用域 (Function Scope)
在函式內部使用 var 宣告的變數,其作用域被限制在該函式內部。這就是函式作用域。在函式外部無法存取這些變數。
function sayHello() {
var greeting = 'Hello, World!';
console.log(greeting); // 可以在函式內部存取
}
sayHello(); // 輸出 'Hello, World!'
console.log(greeting); // ReferenceError: greeting is not defined以上這個範例你可能會想問,如果把 var 換成 let 或 const 不是也成立嗎?其實不是的,如果將 greeting 改成 let 再將它放到一個區塊作用域裡,那麼 greeting 就會變成區塊作用域的變數,而不是函式作用域的變數:
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)
在所有函式和程式碼區塊 ({}) 之外宣告的變數,就擁有全域作用域。這意味著它們可以在程式的任何地方被存取。
// 這是一個全域變數
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 引擎在尋找變數時,會有一套規則:
- 先在當前作用域尋找。
- 如果找不到,就往上一層(外部)作用域尋找。
- 重複這個過程,直到找到變數,或到達最外層的全域作用域。
- 如果最終在全域作用域還是找不到,就會報錯
ReferenceError。
這個由內而外的尋找路徑,就稱為作用域鏈 (Scope Chain)。
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): 在使用
let和const宣告變數時,這些變數在宣告之前是無法存取的。這段期間稱為「暫時性死區 (Temporal Dead Zone, TDZ)」,在這段期間內訪問這些變數會導致ReferenceError。 但是,這個概念只適用於let和const,而var則不受此限制,var宣告的變數會在一開始給予 undefined:
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都會進行提升,但let和const的提升會受到暫時性死區的影響而不能在未宣告時被存取。可重複宣告 (Redeclaration):
var允許在同一作用域內重複宣告變數,而let和const則不允許重複宣告。
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):
var和let宣告的變數可以被重新賦值,而const宣告的變數則不能被重新賦值, 但如果是物件或陣列,其內部屬性或元素是可以被修改的。因為const只保證變數的引用不會改變,但如同資料儲存章節所說的,物件屬性的位址跟物件本身的位址是分開的,所以可以修改物件的屬性或陣列的元素,但不能重新賦值整個物件或陣列。
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!
在實際開發中,建議全部使用 let 和 const 來宣告變數,因為它們提供了更嚴格的作用域控制和更清晰的意圖。var 主要用於舊版 JavaScript 或需要向後相容的情況下使用。
而且 var 在網頁的全域作用域中會被加入到 Window 物件中,配合可以重複宣告的特性,極易導致變數污染和命名衝突,因此非常不推薦在新程式碼中使用 var。
以下是一個 var 變數導致污染的範例:alert 是 Window 物件的預設方法,但我們可以用 var 來宣告一個同名的變數,覆蓋掉 Window 物件的 alert 方法,污染全域作用域:
// 這裡的 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範例練習
預測以下程式碼的輸出結果,並解釋原因。
javascriptlet a = 1; function myFunction() { console.log(a); let b = 2; } myFunction(); // console.log(b); // 這一行會輸出什麼?為什麼?預測以下程式碼的輸出結果。
javascriptlet x = 10; if (true) { let x = 20; console.log(x); // 這裡會輸出什麼? } console.log(x); // 這裡會輸出什麼?以下程式碼有什麼問題?如何修正?
javascriptfor (var i = 0; i < 5; i++) { setTimeout(function() { console.log(i); }, 1000); } // 提示:思考 var 的作用域
