cover

結論先講

三個變數宣告關鍵字:

關鍵字ScopeHoisting重複宣告重新賦值
varFunction✅ 初始化為 undefined✅ 允許✅ 允許
letBlock✅ 但進 TDZ❌ 同 scope 不行✅ 允許
constBlock✅ 但進 TDZ❌ 但物件內容可改

規則:2026 年永遠用 const,需要重新賦值才用 let絕對不用 var

加上原始型別的坑:0.1 + 0.2 ≠ 0.3typeof 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 邊界。iffor{} 這種 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 / constvar 只是你讀老 code 時會遇到。


letconst 的 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 種原始型別:stringnumberbooleannullundefinedsymbolbigint

坑 1:typeof null === 'object'

typeof null;  // 'object'(歷史 bug,永遠不會修)

檢查 null 不要用 typeof,用:

value === null;

坑 2:0.1 + 0.2 === 0.3false

0.1 + 0.2;  // 0.30000000000000004

IEEE 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

宣告規範(現代寫法)

規則

  1. 預設 const — 強制思考「這個值會變嗎」
  2. 需要重新賦值才用 let — 明確告訴讀者這個會變
  3. 絕對不用 var — 只在讀老 code 時接觸
  4. 一行一個宣告 — 不要 let a, b, c;(難讀)
  5. 宣告靠近使用位置 — 不要全部擠頂部(那是 var 時代的遺毒)

命名

  • camelCase:變數、函式
  • PascalCase:class、type
  • UPPER_CASE:真正的常數(數字、設定值)
  • 布林:is/has/can/should 開頭

實戰 Checklist

  • 永遠用 const,需要重新賦值才用 let
  • 不用 var(讀老 code 除外)
  • 浮點運算不要直接比較,錢用整數存
  • 檢查 NaNNumber.isNaN
  • 檢查 null=== null,不用 typeof
  • == null(故意非 !==)同時涵蓋 null + undefined
  • 大整數用 BigInt
  • const 不等於 immutable,需要深凍結用套件

相關文章