JS TDZ 是什麼?搞懂 Temporal Dead Zone,別再被 `let` 和 `const` 偷襲
你以為宣告了就能用?
TL;DR TDZ(Temporal Dead Zone)是指:
let和const從進入作用域開始,到真正執行宣告語句之前,這段時間都「不能被存取」。 所以不是變數不存在,而是它先到了現場,卻還沒開放你跟它講話。 這也是為什麼你會看到ReferenceError,而不是undefined。
因為前陣子寫 VUE 的時候,遇到奇怪的問題(好啦,因為我菜所以算奇怪),
原本寫好的功能正常,但是因為又修正了一些東西,導致我原本可以正常渲染的元件不見了,後來開 DevTools 發現是 ReferenceError,說我在宣告前就存取了變數?
「蛤?JavaScript 不是會 hoisting 嗎?那為什麼我不能先用?」
以下是對話完整理後的筆記…
這個問題很正常。畢竟 JavaScript 在變數這件事上,歷史包袱不少,var、let、const 三個放在一起,真的很像語言設計師曾經開過一場有點混亂的會議。
但先講結論:TDZ 不是 bug,它是故意設計來擋你犯錯的。
TDZ 到底是什麼?
TDZ(Temporal Dead Zone) 是 let 和 const 的一種規則:變數在作用域建立時就已經存在,但在程式真正跑到宣告那一行之前,都不能存取。
你可以把它想成:
- 變數已經進公司了
- 工位也分好了
- 但門禁卡還沒開通
你知道它在那裡,但你就是刷不進去。
如果你看到這裡,心裡已經開始冒出另一個問題:
「所以這是不是跟 hoisting 有關?」
有,而且關係很大。
先用一句白話版講完:
Hoisting 就是 JavaScript 在正式執行前,會先把宣告放進對應作用域。
也因為 let / const 的宣告會先進作用域,但還沒初始化,所以才會出現 TDZ。
如果你想把 hoisting、scope、var / 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 後,先給你undefinedlet:hoist 是有,但不給你碰
所以如果有人跟你說「let 不會 hoist」,這句話不夠精確。
更準確的說法是:let 也會 hoist,但在初始化前會落在 TDZ 裡。
這句才是考試跟實戰都站得住腳的版本。
const 也一樣會進 TDZ
不要以為只有 let 會弄你,const 一樣。
console.log(data);
const data = "abc123";
結果也是:
ReferenceError: Cannot access 'data' before initialization
原因完全相同。
const 跟 let 一樣有區塊作用域,也一樣會進 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
但如果變數是 let 或 const,而且正在 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. 把 const 和 let 盡量放在區塊頂部
不是硬性規定,但很實用。
你把宣告集中在前面,後面邏輯就會單純很多,也比較不會出現「這變數現在到底能不能碰」的窘境。
我會怎麼記這件事?
如果你想用一句話記住 TDZ,我建議記這句:
let和const不是沒 hoist,而是 hoist 之後先把你擋在門外。
這句夠白話,也夠準。
你只要記得:
var:先給undefinedlet/const:先進 TDZ- 等程式跑到宣告那一行,才正式能用
大部分題目就不太會搞混。
常見問題
Q:TDZ 是不是只有 const 才有?
A:不是,let 和 const 都有 TDZ。var 沒有這個機制。
Q:let 是不是不會 hoist?
A:不是。let 也會 hoist,只是在初始化前不能存取,所以你會遇到 TDZ。
Q:為什麼錯誤是 ReferenceError,不是 undefined?
A:因為變數雖然已經存在於作用域中,但在初始化前屬於不可存取狀態。JS 規則要求直接報錯,而不是回傳 undefined。
Q:TDZ 是壞設計嗎?
A:我不這樣看。它確實一開始會讓人覺得機車,但比起 var 那種默默回你 undefined,TDZ 對除錯其實友善得多。
小結
TDZ 的核心其實不複雜:
let 和 const 在宣告前不能用,不是因為它們不存在,而是因為它們正卡在 Temporal Dead Zone。
這個設計的目的,不是故意整你,而是避免你太早碰到一個還沒初始化好的變數。
如果你最近剛好在學 hoisting、scope、closure,TDZ 這個觀念一定要順手一起補起來,不然後面遇到 let / const 的題目,真的很容易被陰一下。
半桶水的
留言區
載入中...
發表留言