6.1 JavaScript 誕生史與語言核心#
十天寫出的語言(1995)#
1995 年 Netscape 要為瀏覽器加腳本能力,Brendan Eich 在十天內設計出 Mocha → LiveScript → JavaScript。名字蹭 Java 的熱度(行銷決定),兩者關係如「雷之兄弟」—— 語法像,本質完全不同。
timeline
title JavaScript 演化
1995 : 十天誕生於 Netscape
1996 : JScript(微軟逆向)→ 瀏覽器大戰,相容性地獄
1997 : ECMA-262 標準化(ECMAScript)
1999 : ES3 — try/catch、regex
2009 : ES5 — strict mode、JSON、map/filter(Ajax 時代基礎)
2009 : Node.js — JS 進入伺服器
2015 : **ES6/ES2015** — class、let/const、箭頭函數、Promise、模組
2015+ : 年度發布制,語言持續演化十天設計的遺產(好壞皆是):
- 原型繼承(借 Self)而非類別 —— 影響深遠
- 一等函數與閉包(借 Scheme)—— 幸運的好決定,是 JS 最強的武器
- 動態型別 + 隱式轉換 —— 幸運的壞決定,
[] + {}之類的怪相由此而來
語言核心:先把 JS 當函數式語言讀#
函數是一等公民 + 閉包#
// 函數可以存進變數、當參數、當回傳值
const add = (a) => (b) => a + b; // 柯里化
add(2)(3); // 5
// 閉包:函數記住定義當下的詞彙環境
function makeCounter() {
let count = 0; // 被內層函數捕獲,不會消失
return () => ++count;
}
const c = makeCounter();
c(); c(); c(); // 1, 2, 3 — 私有狀態!
閉包是 JS 狀態封裝的根基:模組、Hooks(前書 4.2 的 useEffect 依賴捕獲)、事件處理器全靠它。stale closure bug 的根源也在此:函數捕獲的是「變數的引用」而非「當下的值」。
原始型別 vs 物件:複製語意#
| 原始型別(按值) | 物件(按引用) |
|---|---|
| number, string, boolean | object, array, function |
| null, undefined, symbol, bigint | |
a = b 複製值 | a = b 複製引用(同一物件!) |
let a = [1, 2];
let b = a; // b 與 a 指向同一陣列
b.push(3);
a.length; // 3 !(React 不可變更新的原因,前書 4.1)
this:動態綁定的雙面刃#
const user = {
name: 'Amy',
hello() { console.log(`I am ${this.name}`); },
hello2: () => console.log(`I am ${this?.name}`), // 箭頭函數不吃物件的 this
};
user.hello(); // I am Amy
const f = user.hello; f(); // I am undefined(this 丟了!)
this 由呼叫方式決定(動態作用域的殘影),箭頭函數則繼承定義處的 this(詞法 this)。ES6 之後的實務:能箭頭就箭頭,把 this 問題縮小到 class 方法。
型別轉換的坑與防禦#
0 == '' // true(隱式轉換)→ 永遠用 ===
null ?? '預設' // 只有 null/undefined 才回退(vs || 連 0、'' 都回退)
NaN !== NaN // true → 判斷用 Number.isNaN()
typeof null // 'object'(歷史 bug,永遠不修:向後相容鐵律)
ES6 之後的現代語法速覽#
// 解構
const { name, age = 18, ...rest } = user;
const [first, , third] = list;
// 展開
const merged = { ...defaults, ...options }; // 淺合併
const next = [...todos, newTodo]; // 不可變更新
// 模板字串
const html = `<li class="${active ? 'on' : ''}">${name}</li>`;
// 可選鏈與空值合併
const city = user?.address?.city ?? '未知';
// Map/Set(鍵可以是任何型別、有序、高效增刪)
const seen = new Set(ids);
const cache = new Map();
執行模型:單執行緒 + 事件循環(前書 2.3 的 JS 版)#
flowchart TD
A[呼叫堆疊] -->|空了| B[事件循環]
B --> C[微任務佇列<br/>Promise.then / queueMicrotask]
C --> D[巨任務佇列<br/>setTimeout / I/O / 事件]
D --> A優先級:同步碼 → 全部微任務 → 一個巨任務 → 全部微任務 → …
console.log('1');
setTimeout(() => console.log('4'));
Promise.resolve().then(() => console.log('3'));
console.log('2');
// 輸出:1 2 3 4
單執行緒的含義:長任務阻塞一切(包括 UI 回應,4.4 節 INP 指標的敵人)→ 重活用 Worker,I/O 永遠非同步(6.3)。
習題#
- 解釋為什麼
const arr = []; arr.push(1)合法 —— const 鎖的是什麼? - 用閉包寫一個
once(fn):只執行一次、之後回傳快取結果。 - 畫出這段碼的事件循環時序:兩個 setTimeout(0)、三個 Promise.then、一行同步碼。