JS TDZ 是什麼?搞懂 Temporal Dead Zone,別再被 `let` 和 `const` 偷襲

你以為宣告了就能用?

TL;DR TDZ(Temporal Dead Zone)是指:letconst 從進入作用域開始,到真正執行宣告語句之前,這段時間都「不能被存取」。 所以不是變數不存在,而是它先到了現場,卻還沒開放你跟它講話。 這也是為什麼你會看到 ReferenceError,而不是 undefined

因為前陣子寫 VUE 的時候,遇到奇怪的問題(好啦,因為我菜所以算奇怪), 原本寫好的功能正常,但是因為又修正了一些東西,導致我原本可以正常渲染的元件不見了,後來開 DevTools 發現是 ReferenceError,說我在宣告前就存取了變數?

「蛤?JavaScript 不是會 hoisting 嗎?那為什麼我不能先用?」

以下是對話完整理後的筆記…


這個問題很正常。畢竟 JavaScript 在變數這件事上,歷史包袱不少,varletconst 三個放在一起,真的很像語言設計師曾經開過一場有點混亂的會議。

但先講結論:TDZ 不是 bug,它是故意設計來擋你犯錯的。


TDZ 到底是什麼?

TDZ(Temporal Dead Zone)letconst 的一種規則:變數在作用域建立時就已經存在,但在程式真正跑到宣告那一行之前,都不能存取。

你可以把它想成:

  • 變數已經進公司了
  • 工位也分好了
  • 但門禁卡還沒開通

你知道它在那裡,但你就是刷不進去。

如果你看到這裡,心裡已經開始冒出另一個問題:

「所以這是不是跟 hoisting 有關?」

有,而且關係很大。

先用一句白話版講完:

Hoisting 就是 JavaScript 在正式執行前,會先把宣告放進對應作用域。

也因為 let / const 的宣告會先進作用域,但還沒初始化,所以才會出現 TDZ。

如果你想把 hoistingscopevar / let / const 的整張地圖一次看清楚,可以接著讀這篇:

JS Hoisting 與 Scope 一起看:為什麼同一個變數,有時能用有時直接爆?


先看最常見的踩坑範例

先看這段:

console.log(name);

let name = "AJ";

執行後會得到:

ReferenceError: Cannot access 'name' before initialization

很多人會以為 console.log(name) 應該印出 undefined。 很抱歉,這次 JavaScript 不演那套。

因為 name 是用 let 宣告的,所以它在宣告前處於 TDZ。這時候你去讀它,JS 直接翻桌給你看。


為什麼 var 可以,let 不行?

這就是 TDZ 最常被拿來比較的地方。

var 的行為

console.log(score);

var score = 100;

結果:

undefined

原因是 var 會 hoist,而且初始化成 undefined

也就是說,JavaScript 大概會把它理解成這樣:

var score;

console.log(score);

score = 100;

let 的行為

console.log(score);

let score = 100;

結果:

ReferenceError: Cannot access 'score' before initialization

差別就在這裡:

  • var:hoist 後,先給你 undefined
  • let:hoist 是有,但不給你碰

所以如果有人跟你說「let 不會 hoist」,這句話不夠精確。 更準確的說法是:let 也會 hoist,但在初始化前會落在 TDZ 裡。

這句才是考試跟實戰都站得住腳的版本。


const 也一樣會進 TDZ

不要以為只有 let 會弄你,const 一樣。

console.log(data);

const data = "abc123";

結果也是:

ReferenceError: Cannot access 'data' before initialization

原因完全相同。 constlet 一樣有區塊作用域,也一樣會進 TDZ。

只是 const 更嚴格,因為它還要求你宣告時就要初始化。

const data;

這段甚至連執行都不用等,語法階段就直接掛掉:

SyntaxError: Missing initializer in const declaration

TDZ 從什麼時候開始,到什麼時候結束?

這個觀念很重要。

TDZ 的時間範圍是:

從進入作用域開始,到執行宣告語句那一刻為止。

看例子比較清楚:

{
  // TDZ 開始
  console.log(user);

  let user = "AJ";
  console.log(user);
  // TDZ 結束
}

第一個 console.log(user) 會噴錯。 第二個 console.log(user) 才會正常印出:

AJ

所以重點不是「有沒有寫宣告」,而是:

程式執行到那一行了沒。

這也是 TDZ 名字裡 Temporal 的意思。這裡的「時間」不是指幾秒幾分,而是指程式執行順序上的時間區間


為什麼 JavaScript 要設計 TDZ?

我的看法是:這是 JS 難得一次比較像在保護你。

以前 var 最大的問題,就是你很容易在變數還沒準備好時就偷用它,而且程式不一定立刻爆炸,只會默默給你 undefined

這種情況最討厭,因為:

  • 不一定當場錯
  • 後面邏輯才慢慢歪掉
  • 你最後在別的地方 debug 到懷疑人生

TDZ 的設計反而比較誠實:

  • 你先用,我就直接報錯
  • 錯在第一現場
  • 不讓 bug 混進後面流程

老實說,這比「表面沒事,實際埋雷」好多了。


一個很常見的面試題:函式裡面為什麼也會出事?

看這段:

let fruit = "apple";

function showFruit() {
  console.log(fruit);
  let fruit = "banana";
}

showFruit();

有些人會以為它會印出外層的 "apple"。 結果不會,它會直接報錯。

ReferenceError: Cannot access 'fruit' before initialization

原因是當 showFruit() 執行時,函式內的 let fruit 已經建立了自己的區域變數。 從函式作用域開始,到 let fruit = "banana" 執行前,這個內部的 fruit 一直在 TDZ。

也就是說,函式裡的 fruit 已經把外面的 fruit 蓋掉了,只是它自己還沒初始化。

這就像你打電話找老王,結果公司裡剛好也來了一個新老王,但這位新老王還沒辦入職。 你電話已經被轉過去了,只是那邊還沒人能接。


typeof 不是萬能保命符

這點很多人會誤會,因為以前大家會這樣寫:

if (typeof maybeValue !== "undefined") {
  console.log(maybeValue);
}

如果是完全沒宣告過的變數,這樣通常沒事:

console.log(typeof notDeclared);

結果:

undefined

但如果變數是 letconst,而且正在 TDZ 裡:

{
  console.log(typeof token);
  let token = "123";
}

結果還是會報錯:

ReferenceError: Cannot access 'token' before initialization

所以 typeof 在 TDZ 面前,沒有你想像中那麼神。


實務上最容易出現 TDZ 的幾種情況

1. 宣告寫在使用後面

這是最基本,也最常見的版本。

console.log(total);
let total = 10;

2. 區塊內遮蔽外層變數

let mode = "dark";

if (true) {
  console.log(mode);
  let mode = "light";
}

這不是在拿外層的 mode,而是在碰內層還沒初始化的 mode


3. 預設參數互相引用時順序寫錯

function test(a = b, b = 2) {
  return a + b;
}

test();

這段也會炸,因為 a = b 執行時,b 還沒初始化。

ReferenceError

這類題目很愛出現在「你以為自己很懂 JS」的時候。然後它就會提醒你,其實還可以再懂一點。


那到底該怎麼避免 TDZ?

其實不難,規則很樸素: 備註:以下是我比較喜歡的寫法,因為它比較直觀,跟寫 PLC 那種先定義變數再寫邏輯的習慣比較像。

1. 先宣告,再使用

這是最穩的做法。

let price = 200;
console.log(price);

2. 不要在同一個區塊裡用同名變數遮蔽外層

如果真的要用,請讓它出現在更清楚的位置。

let status = "ready";

if (needOverride) {
  let nextStatus = "loading";
  console.log(nextStatus);
}

比起重新宣告一個 status,這種寫法通常更好懂,也比較不會害你自己。


3. 把 constlet 盡量放在區塊頂部

不是硬性規定,但很實用。

你把宣告集中在前面,後面邏輯就會單純很多,也比較不會出現「這變數現在到底能不能碰」的窘境。


我會怎麼記這件事?

如果你想用一句話記住 TDZ,我建議記這句:

letconst 不是沒 hoist,而是 hoist 之後先把你擋在門外。

這句夠白話,也夠準。

你只要記得:

  • var:先給 undefined
  • let / const:先進 TDZ
  • 等程式跑到宣告那一行,才正式能用

大部分題目就不太會搞混。


常見問題

Q:TDZ 是不是只有 const 才有? A:不是,letconst 都有 TDZ。var 沒有這個機制。

Q:let 是不是不會 hoist? A:不是。let 也會 hoist,只是在初始化前不能存取,所以你會遇到 TDZ。

Q:為什麼錯誤是 ReferenceError,不是 undefined A:因為變數雖然已經存在於作用域中,但在初始化前屬於不可存取狀態。JS 規則要求直接報錯,而不是回傳 undefined

Q:TDZ 是壞設計嗎? A:我不這樣看。它確實一開始會讓人覺得機車,但比起 var 那種默默回你 undefined,TDZ 對除錯其實友善得多。


小結

TDZ 的核心其實不複雜:

letconst 在宣告前不能用,不是因為它們不存在,而是因為它們正卡在 Temporal Dead Zone

這個設計的目的,不是故意整你,而是避免你太早碰到一個還沒初始化好的變數。

如果你最近剛好在學 hoisting、scope、closure,TDZ 這個觀念一定要順手一起補起來,不然後面遇到 let / const 的題目,真的很容易被陰一下。

留言區

載入中...

發表留言