本文 AI 產出,尚未審核

JavaScript 變數與資料型別 ── 暫時性死區(Temporal Dead Zone)

簡介

在 ES6 以前,var 是唯一的變數宣告方式,程式執行時只要變數名稱已被解析,就算尚未賦值也不會拋錯。然而 ES6 引入了 letconst 之後,暫時性死區(Temporal Dead Zone,簡稱 TDZ) 成為了必須了解的概念。TDZ 直接關係到變數的可見範圍、提升(hoisting)行為以及程式的執行安全性,若忽略它,常會出現「ReferenceError: Cannot access 'x' before initialization」的錯誤。

本篇文章將從 什麼是 TDZ為什麼會出現如何避免 以及 實務應用 四個面向,完整說明暫時性死區的運作機制,讓讀者在開發 JavaScript 時能寫出更安全、可預測的程式碼。


核心概念

1. TDZ 的定義

暫時性死區是指在程式執行過程中,變數已被宣告但尚未完成初始化的區間。在這段區間內,任何對該變數的讀取或寫入都會拋出 ReferenceError

簡單來說:只要變數使用在它的宣告之前(即使宣告已被提升),都會觸發 TDZ。

2. 為什麼會有 TDZ?

letconst 雖然會被「提升」到所在的作用域(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:varlet 的差異

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

具體的最佳實踐

  1. 宣告靠近使用
    把變數宣告寫在最靠近第一次使用的位置,既避免 TDN,又提升可讀性。

  2. 使用 const 表示常數
    若變數在宣告後不會再被重新賦值,使用 const,它的 TDZ 與 let 相同,但能讓程式意圖更明確。

  3. 避免在預設參數中引用同層 let
    若需要使用外部變數作為預設值,請將變數宣告提升到參數之前,或改用函式內部的條件判斷。

  4. 利用 ESLint 規則
    開啟 no-use-before-defineprefer-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)letconst 變數在 提升 後但 未初始化 前的不可存取區段。
  • TDZ 提升 了變數名稱,但 阻止 任何在宣告之前的存取,從而避免了 var 所帶來的隱式 undefined 問題。
  • const 必須在宣告時就完成賦值,否則會在編譯階段直接錯誤。
  • 常見陷阱包括:在宣告前使用變數、函式參數預設值依賴同層 let、混用 varlet/const 等。
  • 最佳實踐:宣告靠近使用、盡量使用 const、避免在預設參數中引用同層變數、使用 ESLint 捕捉潛在錯誤。

掌握 TDZ 後,你的程式碼將更具可預測性,減少因變數提升而產生的隱藏錯誤,從而寫出更乾淨、更安全的 JavaScript。祝你在開發旅程中順利躲過「暫時性死區」的陷阱,寫出高品質的程式!