
結論先講
三個變數宣告關鍵字:
| 關鍵字 | Scope | Hoisting | 重複宣告 | 重新賦值 |
|---|---|---|---|---|
var | Function | ✅ 初始化為 undefined | ✅ 允許 | ✅ 允許 |
let | Block | ✅ 但進 TDZ | ❌ 同 scope 不行 | ✅ 允許 |
const | Block | ✅ 但進 TDZ | ❌ | ❌ 但物件內容可改 |
規則:2026 年永遠用 const,需要重新賦值才用 let,絕對不用 var。
加上原始型別的坑:0.1 + 0.2 ≠ 0.3、typeof null === 'object'、NaN !== NaN。這些是 JS 世襲的歷史包袱。
為什麼別用 var
問題 1:Function Scope(不是 Block)
function foo() {
if (true) {
var x = 10;
}
console.log(x); // 10,var 會「漏出」block
}var 只認 function 邊界。if、for、{} 這種 block 對它無效。
問題 2:Hoisting 會讓變數「提前存在」
console.log(x); // undefined(不是 ReferenceError)
var x = 10;等同 JS 解釋器在檔案頂部先跑 var x; 讓 x 存在但值是 undefined。程式碼順序跟你以為的不一樣。
問題 3:可以重複宣告
var x = 10;
var x = 20; // 合法,但會讓人困惑大型專案容易誤覆蓋。
結論
現代 JS 永遠用 let / const。var 只是你讀老 code 時會遇到。
let 與 const 的 Block Scope
function foo() {
if (true) {
let x = 10;
}
console.log(x); // ReferenceError - x 超出 scope
}對於迴圈特別重要:
// var:三個 callback 共用同一個 i
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 解決 var 最大痛點的典型例子。
Temporal Dead Zone(TDZ)
let / const 也會被 hoist,但進入 TDZ,用到會噴 ReferenceError:
console.log(x); // ReferenceError: Cannot access 'x' before initialization
let x = 10;TDZ 涵蓋:從 scope 開始 到 變數被宣告的那行為止。這段時間變數「存在但不能用」。
這是好事——它強制你先宣告再使用,避免 var 那種混亂。
const 不是真的「常數」
const obj = { name: 'A' };
obj.name = 'B'; // ✅ 合法!物件內容可改
obj = {}; // ❌ TypeError:不能重新賦值const 只鎖住綁定(reference),不鎖住值。陣列、物件的內容都可改。
要真正 immutable 用 Object.freeze():
const obj = Object.freeze({ name: 'A' });
obj.name = 'B'; // 失敗(嚴格模式噴錯,非嚴格靜默失敗)但 Object.freeze 是淺凍結:
const obj = Object.freeze({ user: { name: 'A' } });
obj.user.name = 'B'; // 仍然合法!內層沒凍結深凍結要自己遞迴處理或用 immutable 套件(Immer、Immutable.js)。
原始型別(Primitives)的 7 個坑
JS 有 7 種原始型別:string、number、boolean、null、undefined、symbol、bigint。
坑 1:typeof null === 'object'
typeof null; // 'object'(歷史 bug,永遠不會修)檢查 null 不要用 typeof,用:
value === null;坑 2:0.1 + 0.2 === 0.3 是 false
0.1 + 0.2; // 0.30000000000000004IEEE 754 浮點的鍋。處理錢一定要用整數(存「分」不存「元」)或用 Decimal 套件。
坑 3:NaN !== NaN
NaN === NaN; // false!NaN 不等於自己檢查 NaN 用:
Number.isNaN(x); // 正確
isNaN(x); // 壞:對非數字字串也回 true(會強制轉型)坑 4:字串不可變(immutable)
let s = 'hello';
s[0] = 'H'; // 靜默失敗
s; // 'hello'要改字串只能用新的:
s = 'H' + s.slice(1); // 'Hello'坑 5:number 最大安全整數
Number.MAX_SAFE_INTEGER; // 9007199254740991(2^53 - 1)
9007199254740993; // 9007199254740992(誤差)超過這範圍要用 BigInt:
9007199254740993n; // BigInt,尾碼 n坑 6:undefined vs null
undefined:宣告但沒賦值(或函式沒回傳)null:明確指定是空
let a; // undefined
let b = null; // null實務建議:用 null,把 undefined 留給「沒設」的狀態。
坑 7:Truthy / Falsy
這些值都會被 if 當 false:
false, 0, -0, 0n, '', null, undefined, NaN
其他全 true。常見坑:
if (value) { ... } // ❌ 萬一 value 是 0 也會被跳過
if (value !== null && value !== undefined) { ... } // ✅ 只檢查 null/undefined
// 或用 nullish
if (value != null) { ... } // != 不是 !==,同時涵蓋 null + undefined宣告規範(現代寫法)
規則
- 預設
const— 強制思考「這個值會變嗎」 - 需要重新賦值才用
let— 明確告訴讀者這個會變 - 絕對不用
var— 只在讀老 code 時接觸 - 一行一個宣告 — 不要
let a, b, c;(難讀) - 宣告靠近使用位置 — 不要全部擠頂部(那是 var 時代的遺毒)
命名
camelCase:變數、函式PascalCase:class、typeUPPER_CASE:真正的常數(數字、設定值)- 布林:
is/has/can/should開頭
實戰 Checklist
- 永遠用
const,需要重新賦值才用let - 不用
var(讀老 code 除外) - 浮點運算不要直接比較,錢用整數存
- 檢查
NaN用Number.isNaN - 檢查
null用=== null,不用typeof - 用
== null(故意非!==)同時涵蓋 null + undefined - 大整數用
BigInt -
const不等於 immutable,需要深凍結用套件
相關文章
- Scope 與 Closure — let/const 的 block scope 如何影響 closure
- JS 子 Roadmap
- TypeScript 基本型別 — 型別系統加持後的版本
- Frontend Roadmap
