2015:ECMAScript 6(ES2015)——現代化的黎明#
事件#
2015年6月,ECMAScript 2015(ES6) 發佈——自 ES5(2009)六年後的最大升級,也是 JavaScript 歷史上規模最大的一次改版。從此標準改為每年發佈一次,並以年份命名。
背景:為何需要 ES6#
- Node.js(2009)讓 JS 跑大型伺服器程式,需要模組、類別、區塊作用域。
- 回呼地獄(callback hell)讓非同步程式碼不可維護,需要 Promise。
var的函數作用域與提升(hoisting)是 bug 溫床。- 框架(Angular、React)大量重複實作 class、模組、模板——這些應該在語言層面解決。
新語法與特性#
1. let / const:塊級作用域#
- 為何加入:
var是函數作用域且會提升,經典 bug:迴圈閉包陷阱、變數在宣告前可用。 - 理論原因:回到 Scheme 的詞法作用域正統——作用域跟著
{}走。 - 實用原因:一次宣告錯誤就能讓數百行程式碼出錯。
- 彌補缺陷:
var的 hoisting 與作用域洩漏。
2. 箭頭函數(=>)#
- 為何加入:ES5 需要寫
function(){}且this會丟失(要var self = this或.bind())。 - 理論原因:λ 演算的簡潔語法;箭頭函數詞法綁定 this(取決外層作用域)。
- 實用原因:回呼成為主流(map/filter、Promise.then),冗長的 function 關鍵字與 this 陷阱天天折磨開發者。
- 彌補缺陷:回呼的 this 丟失與樣板冗長。
3. Promise#
- 為何加入:回呼地獄(巢狀回呼、錯誤要層層傳遞)。
- 理論原因:源自1976年 Promise 理論(Friedman & Wise)與1977年的 futures;Compliance 與 CommonJS Promise/A+ 社群規範先於語言標準。
- 實用原因:Ajax 時代所有 I/O 都是非同步,回呼版本無法組合、無法統一錯誤處理。
- 彌補缺陷:回呼地獄與非同步錯誤處理。
4. class(類別)#
- 為何加入:ES5 的原型繼承要寫
Child.prototype = Object.create(Parent.prototype),且大多數開發者根本寫不對。 - 理論原因:妥協之作——語法糖包裝原型繼承(本質仍是 prototype),並非新增繼承模型。Eich 保留了 Self 的核心,只是加上 SIMULA(1967)式的語法外殼。
- 實用原因:Java/C# 開發者大量湧入(Node 時代),class 語法降低遷移成本。
- 彌補缺陷:原型繼承的可用性。
5. 模組(import / export,ESM)#
- 為何加入:ES 完全沒有模組系統,社群自創 CommonJS(Node)與 AMD(RequireJS),兩者不相容。
- 理論原因:靜態結構——ESM 的 import/export 可在編譯期分析(這是 tree-shaking、迴圈偵測的基礎,CommonJS 做不到)。
- 實用原因:打包工具需要靜態分析;瀏覽器需要無
node_modules的標準模組。 - 彌補缺陷:模組戰國與動態相依的不可分析性。
6. 模板字面量(`)#
- 為何加入:ES5 字串拼接用
+,多行要\n,易錯且醜。 - 實用原因:DOM 模板生成是最大場景;內嵌變數與多行一次解決。
- 彌補缺陷:字串拼接的樣板與錯誤。
7. 解構賦值與展開運算子(...)#
- 理論原因:源自 LISP 的 car/cdr 解構與 ML/Erlang 的模式匹配傳統。
- 實用原因:函數多回傳值、物件取欄位的樣板程式碼。
- 彌補缺陷:
var a = obj.a; var b = obj.b;的樣板。
8. 迭代器與生成器(for...of、function*、yield)#
- 理論原因:Python/CLU 的迭代器協議、Scheme 的 continuation;
yield實現可暫停函數。 - 實用原因:統一遍歷協議(陣列、Map、Set、字串、自訂物件);生成器後來成為 Babel 實作 async/await 的降級目標。
- 彌補缺陷:for-in 遍歷物件屬性的混亂與無統一協議。
9. 其他#
- Map / Set(真正的雜湊表——以前用物件當雜湊表有原型污染與僅字串鍵的缺陷)、
Object.assign、預設參數、其餘參數、Symbol、二進位/八進位字面量、Number.isInteger等。
理論總結#
ES6 是「借鑑各家之長」的大融合:let/const 回歸 Scheme 詞法作用域、class 借 SIMULA 語法、Promise 借 1976 理論、解構借 ML 模式匹配、模組借靜態編譯理論。它沒有發明新概念,而是把三十年來程式語言界的成熟理論補進 JavaScript。
實用總結#
ES6 的落地依賴 Babel 轉譯(2014,見 2014-Babel與webpack轉譯時代)——「社群先試、標準後收」的新模式由此確立。ES6 也標誌著 TC39 轉向年度發佈、開放提案流程的民主化。
相關條目#
程式範例#
1. let/const:塊級作用域消滅 hoisting bug#
// ES5:經典閉包陷阱——印出 3, 3, 3
for (var i = 0; i < 3; i++) {
setTimeout(function () { console.log(i); }, 0);
}
// ES6:let 是塊級作用域——印出 0, 1, 2
for (let i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 0);
}
// const:重新賦值報錯
const PI = 3.14;
PI = 3; // TypeError: Assignment to constant variable
2. 箭頭函數:this 詞法綁定消滅回呼陷阱#
// ES5:回呼中 this 丟失
var timer = {
count: 0,
start: function () {
setInterval(function () { this.count++; }, 1000); // this = window,壞了
}
};
// ES6:箭頭函數的 this 取自外層
var timer = {
count: 0,
start: function () {
setInterval(() => { this.count++; }, 1000); // this = timer,正確
}
};
3. Promise:回呼地獄的解方#
// ES5:回呼地獄(見 2009 Node 範例)
// ES6:Promise 鏈——可組合、統一錯誤處理
fetch("/api/config")
.then((res) => res.json())
.then((cfg) => fetch(cfg.url))
.then((res) => res.json())
.then((data) => console.log(data))
.catch((err) => console.error(err)); // 任何一步失敗都到這裡
4. class:原型繼承的語法糖#
class Point {
constructor(x, y) { this.x = x; this.y = y; }
sum() { return this.x + this.y; }
}
class Point3D extends Point { // 繼承:一行,不用碰 prototype
constructor(x, y, z) {
super(x, y); // 呼叫父類建構
this.z = z;
}
}
new Point3D(1, 2, 3).sum(); // 3
5. 模板字面量、解構、其餘/展開#
const name = "Ada";
const msg = `Hello, ${name}!
多行字串一次搞定`; // 模板字面量
const { name: n, age = 0 } = user; // 解構 + 預設值
const [first, ...rest] = [1, 2, 3]; // 陣列解構 + 其餘
const merged = { ...defaults, ...options }; // 物件展開
6. 迭代器/生成器與 Map/Set#
function* fib() { // 生成器:可暫停函數
let a = 0, b = 1;
while (true) { yield a; [a, b] = [b, a + b]; }
}
for (const v of fib()) { if (v > 100) break; console.log(v); }
const map = new Map(); // 真雜湊表:任意鍵、無原型污染
map.set("x", 1).set(42, "any key"); // 物件當雜湊表做不到
const set = new Set([1, 1, 2]); // set.size === 2