JavaScript 變數與資料型別 ── 暫時性死區(Temporal Dead Zone)
簡介
在 ES6 以前,var 是唯一的變數宣告方式,程式執行時只要變數名稱已被解析,就算尚未賦值也不會拋錯。然而 ES6 引入了 let、const 之後,暫時性死區(Temporal Dead Zone,簡稱 TDZ) 成為了必須了解的概念。TDZ 直接關係到變數的可見範圍、提升(hoisting)行為以及程式的執行安全性,若忽略它,常會出現「ReferenceError: Cannot access 'x' before initialization」的錯誤。
本篇文章將從 什麼是 TDZ、為什麼會出現、如何避免 以及 實務應用 四個面向,完整說明暫時性死區的運作機制,讓讀者在開發 JavaScript 時能寫出更安全、可預測的程式碼。
核心概念
1. TDZ 的定義
暫時性死區是指在程式執行過程中,變數已被宣告但尚未完成初始化的區間。在這段區間內,任何對該變數的讀取或寫入都會拋出 ReferenceError。
簡單來說:只要變數使用在它的宣告之前(即使宣告已被提升),都會觸發 TDZ。
2. 為什麼會有 TDZ?
let、const 雖然會被「提升」到所在的作用域(scope)頂端,但它們的「初始化」只在執行到宣告語句時才發生。提升的結果是變數在宣告前已存在於記憶體中,但仍處於「不可存取」的狀態,這段期間即為 TDZ。
| 變數類型 | 是否提升 | 是否會產生 TDZ |
|---|---|---|
var |
會 | ❌(沒有 TDZ,會得到 undefined) |
let |
會 | ✅(會產生 TDZ) |
const |
會 | ✅(會產生 TDZ) |
3. TDZ 的範圍
TDZ 的範圍 僅限於宣告所在的塊級作用域(block),不會跨越函式或全域作用域。以下圖示說明:
{
// TDZ 開始
console.log(a); // ReferenceError
let a = 10; // 初始化,TDZ 結束
console.log(a); // 10
}
4. const 必須立即初始化
const 宣告的變數在 TDZ 結束時必須已經完成 賦值,否則會在程式結束前拋出 SyntaxError。因此 const 的使用上更需要注意 TDZ。
程式碼範例
範例 1:最基本的 TDZ 錯誤
// 這是一個立即執行函式 (IIFE)
(function () {
console.log(message); // ❌ ReferenceError: Cannot access 'message' before initialization
let message = 'Hello, TDZ!';
})();
- 說明:
let變數message已被提升,但尚未初始化,故在console.log時觸發 TDZ。
範例 2:var 與 let 的差異
function demo() {
console.log(foo); // undefined(var 會自動賦值為 undefined)
console.log(bar); // ❌ ReferenceError
var foo = 'I am var';
let bar = 'I am let';
}
demo();
- 說明:
var foo在提升時已被賦值為undefined,不會產生 TDZ;let bar則會產生 TDZ。
範例 3:TDZ 在迴圈中的常見誤用
for (var i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 0); // 3, 3, 3
}
// 正確寫法:使用 let 讓每次迭代都有自己的 i
for (let i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 0); // 0, 1, 2
}
- 說明:
let在每一次迭代都會重新建立一個區塊作用域,避免了 TDZ 之外的閉包問題,也讓程式行為更直觀。
範例 4:const 必須立即賦值
{
// const a; // ❌ SyntaxError: Missing initializer in const declaration
const b = 5; // 正確
console.log(b); // 5
}
- 說明:
const必須在宣告時就完成初始化,否則會在編譯階段直接報錯。
範例 5:TDZ 與函式參數的互動
function foo(x = y) { // 參數預設值會先於函式內部的 let y 解析
let y = 10;
return x;
}
console.log(foo()); // ❌ ReferenceError: Cannot access 'y' before initialization
- 說明:函式參數的預設值在函式本體執行前就會求值,若預設值依賴於同一作用域內的
let變數,就會觸發 TDZ。
常見陷阱與最佳實踐
| 陷阱 | 可能產生的問題 | 解法 / 最佳實踐 |
|---|---|---|
在宣告前使用 let/const |
ReferenceError |
永遠在使用前先宣告,或把宣告提升到最上方(但仍在同一塊級作用域) |
函式參數預設值依賴同層 let |
TDZ 錯誤 | 將依賴的變數提升到參數之前,或改用 undefined 檢查 |
在 try…catch 裡的 let |
變數被重新宣告導致 TDZ | 盡量避免在同一作用域內重複宣告,使用不同名稱或區塊 |
const 未即時賦值 |
SyntaxError |
宣告即賦值,或改用 let(若稍後才決定值) |
使用 var 混雜 let/const |
行為不一致、難以除錯 | 盡量統一使用 let/const,僅在需要函式作用域或舊版相容時才用 var |
具體的最佳實踐
宣告靠近使用
把變數宣告寫在最靠近第一次使用的位置,既避免 TDN,又提升可讀性。使用
const表示常數
若變數在宣告後不會再被重新賦值,使用const,它的 TDZ 與let相同,但能讓程式意圖更明確。避免在預設參數中引用同層
let
若需要使用外部變數作為預設值,請將變數宣告提升到參數之前,或改用函式內部的條件判斷。利用 ESLint 規則
開啟no-use-before-define、prefer-const等規則,可自動捕捉可能的 TDZ 問題。
實際應用場景
1. 模組化開發(ES Modules)
在模組的頂層,我們常會使用 import 來載入其他模組的變數。import 本身就屬於 TDZ,因為它在模組執行前就被解析,但在模組本身的程式碼執行前仍不可使用。
// utils.js
export const PI = 3.1415;
// main.js
console.log(PI); // ❌ ReferenceError: Cannot access 'PI' before initialization
import { PI } from './utils.js';
解法:把 import 放在檔案最上方,或確保所有 import 在使用前完成。
2. 條件式載入(Dynamic Import)
動態載入時,若在載入完成前就引用目標變數,同樣會觸發 TDZ。
let lib;
if (condition) {
lib = await import('./heavy-lib.js'); // lib 為 Promise 物件
}
console.log(lib.someFunc); // ❌ ReferenceError (lib 尚未初始化)
建議:使用 await 或 .then() 確保載入完成後再使用。
3. 事件處理器內的閉包
在迴圈內建立多個事件監聽器時,使用 let 可避免因 TDZ 之外的閉包問題,確保每個監聽器捕獲正確的變數。
const buttons = document.querySelectorAll('button');
for (let i = 0; i < buttons.length; i++) {
buttons[i].addEventListener('click', () => alert(`Button ${i}`));
}
總結
- 暫時性死區(TDZ) 是
let、const變數在 提升 後但 未初始化 前的不可存取區段。 - TDZ 提升 了變數名稱,但 阻止 任何在宣告之前的存取,從而避免了
var所帶來的隱式undefined問題。 const必須在宣告時就完成賦值,否則會在編譯階段直接錯誤。- 常見陷阱包括:在宣告前使用變數、函式參數預設值依賴同層
let、混用var與let/const等。 - 最佳實踐:宣告靠近使用、盡量使用
const、避免在預設參數中引用同層變數、使用 ESLint 捕捉潛在錯誤。
掌握 TDZ 後,你的程式碼將更具可預測性,減少因變數提升而產生的隱藏錯誤,從而寫出更乾淨、更安全的 JavaScript。祝你在開發旅程中順利躲過「暫時性死區」的陷阱,寫出高品質的程式!