# 第三章:組織
編寫JS代碼是一回事兒,而合理地組織它是另一回事兒。利用常見的組織和重用模式在很大程度上改善了你代碼的可讀性和可理解性。記住:代碼在與其他開發者交流上起的作用,與在給計算機喂指令上起的作用同樣重要。
ES6擁有幾種重要的特性可以顯著改善這些模式,包括:迭代器,generator,模塊,和類。
## 迭代器
*迭代器(iterator)*?是一種結構化的模式,用于從一個信息源中以一次一個的方式抽取信息。這種模式在程序設計中存在很久了。而且不可否認的是,不知從什么時候起JS開發者們就已經特別地設計并實現了迭代器,所以它根本不是什么新的話題。
ES6所做的是,為迭代器引入了一個隱含的標準化接口。許多在JavaScript中內建的數據結構現在都會暴露一個實現了這個標準的迭代器。而且你也可以構建自己的遵循同樣標準的迭代器,來使互用性最大化。
迭代器是一種消費數據的方法,它是組織有順序的,相繼的,基于抽取的。
舉個例子,你可能實現一個工具,它在每次被請求時產生一個新的唯一的標識符。或者你可能循環一個固定的列表以輪流的方式產生一系列無限多的值。或者你可以在一個數據庫查詢的結果上添加一個迭代器來一次抽取一行結果。
雖然在JS中它們不經常以這樣的方式被使用,但是迭代器還可以認為是每次控制行為中的一個步驟。這會在考慮generator時得到相當清楚的展示(參見本章稍后的“Generator”),雖然你當然可以不使用generator而做同樣的事。
### 接口
在本書寫作的時候,ES6的25.1.1.2部分 ([https://people.mozilla.org/~jorendorff/es6-draft.html#sec-iterator-interface](https://people.mozilla.org/~jorendorff/es6-draft.html#sec-iterator-interface)) 詳述了`Iterator`接口,它有如下的要求:
~~~
Iterator [必須]
next() {method}: 取得下一個IteratorResult
~~~
有兩個可選成員,有些迭代器用它們進行了擴展:
~~~
Iterator [可選]
return() {method}: 停止迭代并返回IteratorResult
throw() {method}: 通知錯誤并返回IteratorResult
~~~
接口`IteratorResult`被規定為:
~~~
IteratorResult
value {property}: 當前的迭代值或最終的返回值
(如果它的值為`undefined`,是可選的)
done {property}: 布爾值,指示完成的狀態
~~~
注意:?我稱這些接口是隱含的,不是因為它們沒有在語言規范中被明確地被說出來 —— 它們被說出來了!—— 而是因為它們沒有作為可以直接訪問的對象暴露給代碼。在ES6中,JavaScript不支持任何“接口”的概念,所以在你自己的代碼中遵循它們純粹是慣例上的。但是,不論JS在何處需要一個迭代器 —— 例如在一個`for..of`循環中 —— 你提供的東西必須遵循這些接口,否則代碼就會失敗。
還有一個`Iterable`接口,它描述了一定能夠產生迭代器的對象:
~~~
Iterable
@@iterator() {method}: 產生一個迭代器
~~~
如果你回憶一下第二章的“內建Symbol”,`@@iterator`是一種特殊的內建symbol,表示可以為對象產生迭代器的方法。
#### IteratorResult
`IteratorResult`接口規定從任何迭代器操作的返回值都是這樣形式的對象:
```source-js
{ value: .. , done: true / false }
```
內建迭代器將總是返回這種形式的值,當然,更多的屬性也允許出現在這個返回值中,如果有必要的話。
例如,一個自定義的迭代器可能會在結果對象中加入額外的元數據(比如,數據是從哪里來的,取得它花了多久,緩存過期的時間長度,下次請求的恰當頻率,等等)。
注意:?從技術上講,在值為`undefined`的情況下,`value`是可選的,它將會被認為是不存在或者是沒有被設置。因為不管它是表示的就是這個值還是完全不存在,訪問`res.value`都將會產生`undefined`,所以這個屬性的存在/不存在更大程度上是一個實現或者優化(或兩者)的細節,而非一個功能上的問題。
### `next()`迭代
讓我們來看一個數組,它是一個可迭代對象,可以生成一個迭代器來消費它的值:
```source-js
var arr = [1,2,3];
var it = arr[Symbol.iterator]();
it.next(); // { value: 1, done: false }
it.next(); // { value: 2, done: false }
it.next(); // { value: 3, done: false }
it.next(); // { value: undefined, done: true }
```
每一次定位在`Symbol.iterator`上的方法在值`arr`上被調用時,它都將生成一個全新的迭代器。大多數的數據結構都會這么做,包括所有內建在JS中的數據結構。
然而,像事件隊列這樣的結構也許只能生成一個單獨的迭代器(單例模式)。或者某種結構可能在同一時間內只允許存在一個唯一的迭代器,要求當前的迭代器必須完成,才能創建一個新的。
前一個代碼段中的`it`迭代器不會再你得到值`3`時報告`done: true`。你必須再次調用`next()`,實質上越過數組末尾的值,才能得到完成信號`done: true`。在這一節稍后會清楚地講解這種設計方式的原因,但是它通常被認為是一種最佳實踐。
基本類型的字符串值也默認地是可迭代對象:
```source-js
var greeting = "hello world";
var it = greeting[Symbol.iterator]();
it.next(); // { value: "h", done: false }
it.next(); // { value: "e", done: false }
..
```
注意:?從技術上講,這個基本類型值本身不是可迭代對象,但多虧了“封箱”,`"hello world"`被強制轉換為它的`String`對象包裝形式,*它*?才是一個可迭代對象。更多信息參見本系列的?*類型與文法*。
ES6還包括幾種新的數據結構,稱為集合(參見第五章)。這些集合不僅本身就是可迭代對象,而且它們還提供API方法來生成一個迭代器,例如:
```source-js
var m = new Map();
m.set( "foo", 42 );
m.set( { cool: true }, "hello world" );
var it1 = m[Symbol.iterator]();
var it2 = m.entries();
it1.next(); // { value: [ "foo", 42 ], done: false }
it2.next(); // { value: [ "foo", 42 ], done: false }
..
```
一個迭代器的`next(..)`方法能夠可選地接受一個或多個參數。大多數內建的迭代器不會實施這種能力,雖然一個generator的迭代器絕對會這么做(參見本章稍后的“Generator”)。
根據一般的慣例,包括所有的內建迭代器,在一個已經被耗盡的迭代器上調用`next(..)`不是一個錯誤,而是簡單地持續返回結果`{ value: undefined, done: true }`。
### 可選的`return(..)`和`throw(..)`
在迭代器接口上的可選方法 ——?`return(..)`和`throw(..)`?—— 在大多數內建的迭代器上都沒有被實現。但是,它們在generator的上下文環境中絕對有某些含義,所以更具體的信息可以參看“Generator”。
`return(..)`被定義為向一個迭代器發送一個信號,告知它消費者代碼已經完成而且不會再從它那里抽取更多的值。這個信號可以用于通知生產者(應答`next(..)`調用的迭代器)去實施一些可能的清理作業,比如釋放/關閉網絡,數據庫,或者文件引用資源。
如果一個迭代器擁有`return(..)`,而且發生了可以自動被解釋為非正常或者提前終止消費迭代器的任何情況,`return(..)`就將會被自動調用。你也可以手動調用`return(..)`。
`return(..)`將會像`next(..)`一樣返回一個`IteratorResult`對象。一般來說,你向`return(..)`發送的可選值將會在這個`IteratorResult`中作為`value`發送回來,雖然在一些微妙的情況下這可能不成立。
`throw(..)`被用于向一個迭代器發送一個異常/錯誤信號,與`return(..)`隱含的完成信號相比,它可能會被迭代器用于不同的目的。它不一定像`return(..)`一樣暗示著迭代器的完全停止。
例如,在generator迭代器中,`throw(..)`實際上會將一個被拋出的異常注射到generator暫停的執行環境中,這個異常可以用`try..catch`捕獲。一個未捕獲的`throw(..)`異常將會導致generator的迭代器異常中止。
注意:?根據一般的慣例,在`return(..)`或`throw(..)`被調用之后,一個迭代器就不應該在產生任何結果了。
### 迭代器循環
正如我們在第二章的“`for..of`”一節中講解的,ES6的`for..of`循環可以直接消費一個規范的可迭代對象。
如果一個迭代器也是一個可迭代對象,那么它就可以直接與`for..of`循環一起使用。通過給予迭代器一個簡單地返回它自身的`Symbol.iterator`方法,你就可以使它成為一個可迭代對象:
```source-js
var it = {
// 使迭代器`it`成為一個可迭代對象
[Symbol.iterator]() { return this; },
next() { .. },
..
};
it[Symbol.iterator]() === it; // true
```
現在我們就可以用一個`for..of`循環來消費迭代器`it`了:
```source-js
for (var v of it) {
console.log( v );
}
```
為了完全理解這樣的循環如何工作,回憶下第二章中的`for..of`循環的`for`等價物:
```source-js
for (var v, res; (res = it.next()) && !res.done; ) {
v = res.value;
console.log( v );
}
```
如果你仔細觀察,你會發現`it.next()`是在每次迭代之前被調用的,然后`res.done`才被查詢。如果`res.done`是`true`,那么這個表達式將會求值為`false`于是這次迭代不會發生。
回憶一下之前我們建議說,迭代器一般不應與最終預期的值一起返回`done: true`。現在你知道為什么了。
如果一個迭代器返回了`{ done: true, value: 42 }`,`for..of`循環將完全扔掉值`42`。因此,假定你的迭代器可能會被`for..of`循環或它的`for`等價物這樣的模式消費的話,你可能應當等到你已經返回了所有相關的迭代值之后才返回`done: true`來表示完成。
警告:?當然,你可以有意地將你的迭代器設計為將某些相關的`value`與`done: true`同時返回。但除非你將此情況在文檔中記錄下來,否則不要這么做,因為這樣會隱含地強制你的迭代器消費者使用一種,與我們剛才描述的`for..of`或它的手動等價物不同的模式來進行迭代。
### 自定義迭代器
除了標準的內建迭代器,你還可以制造你自己的迭代器!所有使它們可以與ES6消費設施(例如,`for..of`循環和`...`操作符)進行互動的代價就是遵循恰當的接口。
讓我們試著構建一個迭代器,它能夠以斐波那契(Fibonacci)數列的形式產生無限多的數字序列:
```source-js
var Fib = {
[Symbol.iterator]() {
var n1 = 1, n2 = 1;
return {
// 使迭代器成為一個可迭代對象
[Symbol.iterator]() { return this; },
next() {
var current = n2;
n2 = n1;
n1 = n1 + current;
return { value: current, done: false };
},
return(v) {
console.log(
"Fibonacci sequence abandoned."
);
return { value: v, done: true };
}
};
}
};
for (var v of Fib) {
console.log( v );
if (v > 50) break;
}
// 1 1 2 3 5 8 13 21 34 55
// Fibonacci sequence abandoned.
```
警告:?如果我們沒有插入`break`條件,這個`for..of`循環將會永遠運行下去,這回破壞你的程序,因此可能不是我們想要的!
方法`Fib[Symbol.iterator]()`在被調用時返回帶有`next()`和`return(..)`方法的迭代器對象。它的狀態通過變量`n1`和`n2`維護在閉包中。
接下來讓我們考慮一個迭代器,它被設計為執行一系列(也叫隊列)動作,一次一個:
```source-js
var tasks = {
[Symbol.iterator]() {
var steps = this.actions.slice();
return {
// 使迭代器成為一個可迭代對象
[Symbol.iterator]() { return this; },
next(...args) {
if (steps.length > 0) {
let res = steps.shift()( ...args );
return { value: res, done: false };
}
else {
return { done: true }
}
},
return(v) {
steps.length = 0;
return { value: v, done: true };
}
};
},
actions: []
};
```
在`tasks`上的迭代器步過在數組屬性`actions`中找到的函數,并每次執行它們中的一個,并傳入你傳遞給`next(..)`的任何參數值,并在標準的`IteratorResult`對象中向你返回任何它返回的東西。
這是我們如何使用這個`tasks`隊列:
```source-js
tasks.actions.push(
function step1(x){
console.log( "step 1:", x );
return x * 2;
},
function step2(x,y){
console.log( "step 2:", x, y );
return x + (y * 2);
},
function step3(x,y,z){
console.log( "step 3:", x, y, z );
return (x * y) + z;
}
);
var it = tasks[Symbol.iterator]();
it.next( 10 ); // step 1: 10
// { value: 20, done: false }
it.next( 20, 50 ); // step 2: 20 50
// { value: 120, done: false }
it.next( 20, 50, 120 ); // step 3: 20 50 120
// { value: 1120, done: false }
it.next(); // { done: true }
```
這種特別的用法證實了迭代器可以是一種具有組織功能的模式,不僅僅是數據。這也聯系著我們在下一節關于generator將要看到的東西。
你甚至可以更有創意一些,在一塊數據上定義一個表示元操作的迭代器。例如,我們可以為默認從0開始遞增至(或遞減至,對于負數來說)指定數字的一組數字定義一個迭代器。
考慮如下代碼:
```source-js
if (!Number.prototype[Symbol.iterator]) {
Object.defineProperty(
Number.prototype,
Symbol.iterator,
{
writable: true,
configurable: true,
enumerable: false,
value: function iterator(){
var i, inc, done = false, top = +this;
// 正向迭代還是負向迭代?
inc = 1 * (top < 0 ? -1 : 1);
return {
// 使迭代器本身成為一個可迭代對象!
[Symbol.iterator](){ return this; },
next() {
if (!done) {
// 最初的迭代總是0
if (i == null) {
i = 0;
}
// 正向迭代
else if (top >= 0) {
i = Math.min(top,i + inc);
}
// 負向迭代
else {
i = Math.max(top,i + inc);
}
// 這次迭代之后就完了?
if (i == top) done = true;
return { value: i, done: false };
}
else {
return { done: true };
}
}
};
}
}
);
}
```
現在,這種創意給了我們什么技巧?
```source-js
for (var i of 3) {
console.log( i );
}
// 0 1 2 3
[...-3]; // [0,-1,-2,-3]
```
這是一些有趣的技巧,雖然其實際用途有些值得商榷。但是再一次,有人可能想知道為什么ES6沒有提供如此微小但討喜的特性呢?
如果我連這樣的提醒都沒給過你,那就是我的疏忽:像我在前面的代碼段中做的那樣擴展原生原型,是一件你需要小心并了解潛在的危害后才應該做的事情。
在這樣的情況下,你與其他代碼或者未來的JS特性發生沖突的可能性非常低。但是要小心微小的可能性。并在文檔中為后人詳細記錄下你在做什么。
注意:?如果你想知道更多細節,我在這篇文章([http://blog.getify.com/iterating-es6-numbers/](http://blog.getify.com/iterating-es6-numbers/)) 中詳細論述了這種特別的技術。而且這段評論([http://blog.getify.com/iterating-es6-numbers/comment-page-1/#comment-535294)甚至為制造一個字符串字符范圍提出了一個相似的技巧。](http://blog.getify.com/iterating-es6-numbers/comment-page-1/#comment-535294)
### 消費迭代器
我們已經看到了使用`for..of`循環來一個元素一個元素地消費一個迭代器。但是還有一些其他的ES6結構可以消費迭代器。
讓我們考慮一下附著這個數組上的迭代器(雖然任何我們選擇的迭代器都將擁有如下的行為):
```source-js
var a = [1,2,3,4,5];
```
擴散操作符`...`將完全耗盡一個迭代器。考慮如下代碼:
```source-js
function foo(x,y,z,w,p) {
console.log( x + y + z + w + p );
}
foo( ...a ); // 15
```
`...`還可以在一個數組內部擴散一個迭代器:
```source-js
var b = [ 0, ...a, 6 ];
b; // [0,1,2,3,4,5,6]
```
數組解構(參見第二章的“解構”)可以部分地或者完全地(如果與一個`...`剩余/收集操作符一起使用)消費一個迭代器:
```source-js
var it = a[Symbol.iterator]();
var [x,y] = it; // 僅從`it`中取前兩個元素
var [z, ...w] = it; // 取第三個,然后一次取得剩下所有的
// `it`被完全耗盡了嗎?是的
it.next(); // { value: undefined, done: true }
x; // 1
y; // 2
z; // 3
w; // [4,5]
```
## Generator
所有的函數都會運行至完成,對吧?換句話說,一旦一個函數開始運行,在它完成之前沒有任何東西能夠打斷它。
至少對于到目前為止的JavaScript的整個歷史來說是這樣的。在ES6中,引入了一個有些異乎尋常的新形式的函數,稱為generator。一個generator可以在運行期間暫停它自己,還可以立即或者稍后繼續運行。所以顯然它沒有普通函數那樣的運行至完成的保證。
另外,在運行期間的每次暫停/繼續輪回都是一個雙向消息傳遞的好機會,generator可以在這里返回一個值,而使它繼續的控制端代碼可以發回一個值。
就像前一節中的迭代器一樣,有種方式可以考慮generator是什么,或者說它對什么最有用。對此沒有一個正確的答案,但我們將試著從幾個角度考慮。
注意:?關于generator的更多信息參見本系列的?*異步與性能*,還可以參見本書的第四章。
### 語法
generator函數使用這種新語法聲明:
```source-js
function *foo() {
// ..
}
```
`*`的位置在功能上無關緊要。同樣的聲明還可以寫做以下的任意一種:
```source-js
function *foo() { .. }
function* foo() { .. }
function * foo() { .. }
function*foo() { .. }
..
```
這里?*唯一*?的區別就是風格的偏好。大多數其他的文獻似乎喜歡`function* foo(..) { .. }`。我喜歡`function *foo(..) { .. }`,所以這就是我將在本書剩余部分中表示它們的方法。
我這樣做的理由實質上純粹是為了教學。在這本書中,當我引用一個generator函數時,我將使用`*foo(..)`,與普通函數的`foo(..)`相對。我發現`*foo(..)`與`function *foo(..) { .. }`中`*`的位置更加吻合。
另外,就像我們在第二章的簡約方法中看到的,在對象字面量中有一種簡約generator形式:
```source-js
var a = {
*foo() { .. }
};
```
我要說在簡約generator中,`*foo() { .. }`要比`* foo() { .. }`更自然。這進一步表明了為何使用`*foo()`匹配一致性。
一致性使理解與學習更輕松。
#### 執行一個Generator
雖然一個generator使用`*`進行聲明,但是你依然可以像一個普通函數那樣執行它:
```source-js
foo();
```
你依然可以傳給它參數值,就像:
```source-js
function *foo(x,y) {
// ..
}
foo( 5, 10 );
```
主要區別在于,執行一個generator,比如`foo(5,10)`,并不實際運行generator中的代碼。取而代之的是,它生成一個迭代器來控制generator執行它的代碼。
我們將在稍后的“迭代器控制”中回到這個話題,但是簡要地說:
```source-js
function *foo() {
// ..
}
var it = foo();
// 要開始/推進`*foo()`,調用
// `it.next(..)`
```
#### `yield`
Generator還有一個你可以在它們內部使用的新關鍵字,用來表示暫停點:`yield`。考慮如下代碼:
```source-js
function *foo() {
var x = 10;
var y = 20;
yield;
var z = x + y;
}
```
在這個`*foo()`generator中,前兩行的操作將會在開始時運行,然后`yield`將會暫停這個generator。如果這個generator被繼續,`*foo()`的最后一行將運行。在一個generator中`yield`可以出現任意多次(或者,在技術上講,根本不出現!)。
你甚至可以在一個循環內部放置`yield`,它可以表示一個重復的暫停點。事實上,一個永不完成的循環就意味著一個永不完成的generator,這是完全合法的,而且有時候完全是你需要的。
`yield`不只是一個暫停點。它是在暫停generator時發送出一個值的表達式。這里是一個位于generator中的`while..true`循環,它每次迭代時`yield`出一個新的隨機數:
```source-js
function *foo() {
while (true) {
yield Math.random();
}
}
```
`yield ..`表達式不僅發送一個值 —— 不帶值的`yield`與`yield undefined`相同 —— 它還接收(也就是,被替換為)最終的繼續值。考慮如下代碼:
```source-js
function *foo() {
var x = yield 10;
console.log( x );
}
```
這個generator在暫停它自己時將首先`yield`出值`10`。當你繼續這個generator時 —— 使用我們先前提到的`it.next(..)`?—— 無論你使用什么值繼續它,這個值都將替換/完成整個表達式`yield 10`,這意味著這個值將被賦值給變量`x`
一個`yield..`表達式可以出現在任意普通表達式可能出現的地方。例如:
```source-js
function *foo() {
var arr = [ yield 1, yield 2, yield 3 ];
console.log( arr, yield 4 );
}
```
這里的`*foo()`有四個`yield ..`表達式。其中每個`yield`都會導致generator暫停以等待一個繼續值,這個繼續值稍后被用于各個表達式環境中。
`yield`在技術上講不是一個操作符,雖然像`yield 1`這樣使用時看起來確實很像。因為`yield`可以像`var x = yield`這樣完全通過自己被使用,所以將它認為是一個操作符有時令人困惑。
從技術上講,`yield ..`與`a = 3`這樣的賦值表達式擁有相同的“表達式優先級” —— 概念上和操作符優先級很相似。這意味著`yield ..`基本上可以出現在任何`a = 3`可以合法出現的地方。
讓我們展示一下這種對稱性:
```source-js
var a, b;
a = 3; // 合法
b = 2 + a = 3; // 不合法
b = 2 + (a = 3); // 合法
yield 3; // 合法
a = 2 + yield 3; // 不合法
a = 2 + (yield 3); // 合法
```
注意:?如果你好好考慮一下,認為一個`yield ..`表達式與一個賦值表達式的行為相似在概念上有些道理。當一個被暫停的generator被繼續時,它就以一種與被這個繼續值“賦值”區別不大的方式,被這個值完成/替換。
要點:如果你需要`yield ..`出現在`a = 3`這樣的賦值本不被允許出現的位置,那么它就需要被包在一個`( )`中。
因為`yield`關鍵字的優先級很低,幾乎任何出現在`yield ..`之后的表達式都會在被`yield`發送之前首先被計算。只有擴散操作符`...`和逗號操作符`,`擁有更低的優先級,這意味著他們會在`yield`已經被求值之后才會被處理。
所以正如帶有多個操作符的普通語句一樣,存在另一個可能需要`( )`來覆蓋(提升)`yield`的低優先級的情況,就像這些表達式之間的區別:
```source-js
yield 2 + 3; // 與`yield (2 + 3)`相同
(yield 2) + 3; // 首先`yield 2`,然后`+ 3`
```
和`=`賦值一樣,`yield`也是“右結合性”的,這意味著多個接連出現的`yield`表達式被視為從右到左被`( .. )`分組。所以,`yield yield yield 3`將被視為`yield (yield (yield 3))`。像`((yield) yield) yield 3`這樣的“左結合性”解釋沒有意義。
和其他操作符一樣,`yield`與其他操作符或`yield`組合時為了使你的意圖沒有歧義,使用`( .. )`分組是一個好主意,即使這不是嚴格要求的。
注意:?更多關于操作符優先級和結合性的信息,參見本系列的?*類型與文法*。
#### `yield *`
與`*`使一個`function`聲明成為一個`function *`generator聲明的方式一樣,一個`*`使`yield`成為一個機制非常不同的`yield *`,稱為?*yield委托*。從文法上講,`yield *..`的行為與`yield ..`相同,就像在前一節討論過的那樣。
`yield * ..`需要一個可迭代對象;然后它調用這個可迭代對象的迭代器,并將它自己的宿主generator的控制權委托給那個迭代器,直到它被耗盡。考慮如下代碼:
```source-js
function *foo() {
yield *[1,2,3];
}
```
注意:?與generator聲明中`*`的位置(早先討論過)一樣,在`yield *`表達式中的`*`的位置在風格上由你來決定。大多數其他文獻偏好`yield* ..`,但是我喜歡`yield *..`,理由和我們已經討論過的相同。
值`[1,2,3]`產生一個將會步過它的值的迭代器,所以generator`*foo()`將會在被消費時產生這些值。另一種說明這種行為的方式是,yield委托到了另一個generator:
```source-js
function *foo() {
yield 1;
yield 2;
yield 3;
}
function *bar() {
yield *foo();
}
```
當`*bar()`調用`*foo()`產生的迭代器通過`yield *`受到委托,意味著無論`*foo()`產生什么值都會被`*bar()`產生。
在`yield ..`中表達式的完成值來自于使用`it.next(..)`繼續generator,而`yield *..`表達式的完成值來自于受到委托的迭代器的返回值(如果有的話)。
內建的迭代器一般沒有返回值,正如我們在本章早先的“迭代器循環”一節的末尾講過的。但是如果你定義你自己的迭代器(或者generator),你就可以將它設計為`return`一個值,`yield *..`將會捕獲它:
```source-js
function *foo() {
yield 1;
yield 2;
yield 3;
return 4;
}
function *bar() {
var x = yield *foo();
console.log( "x:", x );
}
for (var v of bar()) {
console.log( v );
}
// 1 2 3
// x: 4
```
雖然值`1`,`2`,和`3`從`*foo()`中被`yield`出來,然后從`*bar()`中被`yield`出來,但是從`*foo()`中返回的值`4`是表達式`yield *foo()`的完成值,然后它被賦值給`x`。
因為`yield *`可以調用另一個generator(通過委托到它的迭代器的方式),它還可以通過調用自己來實施某種generator遞歸:
```source-js
function *foo(x) {
if (x < 3) {
x = yield *foo( x + 1 );
}
return x * 2;
}
foo( 1 );
```
取得`foo(1)`的結果并調用迭代器的`next()`來使它運行它的遞歸步驟,結果將是`24`。第一次`*foo()`運行時`x`擁有值`1`,它是`x < 3`。`x + 1`被遞歸地傳遞到`*foo(..)`,所以之后的`x`是`2`。再一次遞歸調用導致`x`為`3`。
現在,因為`x < 3`失敗了,遞歸停止,而且`return 3 * 2`將`6`給回前一個調用的`yeild *..`表達式,它被賦值給`x`。另一個`return 6 * 2`返回`12`給前一個調用的`x`。最終`12 * 2`,即`24`,從generator`*foo(..)`運行的完成中被返回。
### 迭代器控制
早先,我們簡要地介紹了generator是由迭代器控制的概念。現在讓我們完整地深入這個話題。
回憶一下前一節的遞歸`*for(..)`。這是我們如何運行它:
```source-js
function *foo(x) {
if (x < 3) {
x = yield *foo( x + 1 );
}
return x * 2;
}
var it = foo( 1 );
it.next(); // { value: 24, done: true }
```
在這種情況下,generator并沒有真正暫停過,因為這里沒有`yield ..`表達式。而`yield *`只是通過遞歸調用保持當前的迭代步驟繼續運行下去。所以,僅僅對迭代器的`next()`函數進行一次調用就完全地運行了generator。
現在讓我們考慮一個有多個步驟并且因此有多個產生值的generator:
```source-js
function *foo() {
yield 1;
yield 2;
yield 3;
}
```
我們已經知道我們可以是使用一個`for..of`循環來消費一個迭代器,即便它是一個附著在`*foo()`這樣的generator上:
```source-js
for (var v of foo()) {
console.log( v );
}
// 1 2 3
```
注意:?`for..of`循環需要一個可迭代對象。一個generator函數引用(比如`foo`)本身不是一個可迭代對象;你必須使用`foo()`來執行它以得到迭代器(它也是一個可迭代對象,正如我們在本章早先講解過的)。理論上你可以使用一個實質上僅僅執行`return this()`的`Symbol.iterator`函數來擴展`GeneratorPrototype`(所有generator函數的原型)。這將使`foo`引用本身成為一個可迭代對象,也就意味著`for (var v of foo) { .. }`(注意在`foo`上沒有`()`)將可以工作。
讓我們手動迭代這個generator:
```source-js
function *foo() {
yield 1;
yield 2;
yield 3;
}
var it = foo();
it.next(); // { value: 1, done: false }
it.next(); // { value: 2, done: false }
it.next(); // { value: 3, done: false }
it.next(); // { value: undefined, done: true }
```
如果你仔細觀察,這里有三個`yield`語句和四個`next()`調用。這可能看起來像是一個奇怪的不匹配。事實上,假定所有的東西都被求值并且generator完全運行至完成的話,`next()`調用將總是比`yield`表達式多一個。
但是如果你相反的角度觀察(從里向外而不是從外向里),`yield`和`next()`之間的匹配就顯得更有道理。
回憶一下,`yield ..`表達式將被你用于繼續generator的值完成。這意味著你傳遞給`next(..)`的參數值將完成任何當前暫停中等待完成的`yield ..`表達式。
讓我們這樣展示一下這種視角:
```source-js
function *foo() {
var x = yield 1;
var y = yield 2;
var z = yield 3;
console.log( x, y, z );
}
```
在這個代碼段中,每個`yield ..`都送出一個值(`1`,`2`,`3`),但更直接的是,它暫停了generator來等待一個值。換句話說,它就像在問這樣一個問題,“我應當在這里用什么值?我會在這里等你告訴我。”
現在,這是我們如何控制`*foo()`來啟動它:
```source-js
var it = foo();
it.next(); // { value: 1, done: false }
```
這第一個`next()`調用從generator初始的暫停狀態啟動了它,并運行至第一個`yield`。在你調用第一個`next()`的那一刻,并沒有`yield ..`表達式等待完成。如果你給第一個`next()`調用傳遞一個值,目前它會被扔掉,因為沒有`yield`等著接受這樣的一個值。
注意:?一個“ES6之后”時間表中的早期提案?*將*?允許你在generator內部通過一個分離的元屬性(見第七章)來訪問一個被傳入初始`next(..)`調用的值。
現在,讓我們回答那個未解的問題,“我應當給`x`賦什么值?” 我們將通過給?*下一個*?`next(..)`調用發送一個值來回答:
```source-js
it.next( "foo" ); // { value: 2, done: false }
```
現在,`x`將擁有值`"foo"`,但我們也問了一個新的問題,“我應當給`y`賦什么值?”
```source-js
it.next( "bar" ); // { value: 3, done: false }
```
答案給出了,另一個問題被提出了。最終答案:
```source-js
it.next( "baz" ); // "foo" "bar" "baz"
// { value: undefined, done: true }
```
現在,每一個`yield ..`的“問題”是如何被?*下一個*?`next(..)`調用回答的,所以我們觀察到的那個“額外的”`next()`調用總是使一切開始的那一個。
讓我們把這些步驟放在一起:
```source-js
var it = foo();
// 啟動generator
it.next(); // { value: 1, done: false }
// 回答第一個問題
it.next( "foo" ); // { value: 2, done: false }
// 回答第二個問題
it.next( "bar" ); // { value: 3, done: false }
// 回答第三個問題
it.next( "baz" ); // "foo" "bar" "baz"
// { value: undefined, done: true }
```
在生成器的每次迭代都簡單地為消費者生成一個值的情況下,你可認為一個generator是一個值的生成器。
但是在更一般的意義上,也許將generator認為是一個受控制的,累進的代碼執行過程更恰當,與早先“自定義迭代器”一節中的`tasks`隊列的例子非常相像。
注意:?這種視角正是我們將如何在第四章中重溫generator的動力。特別是,`next(..)`沒有理由一定要在前一個`next(..)`完成之后立即被調用。雖然generator的內部執行環境被暫停了,程序的其他部分仍然沒有被阻塞,這包括控制generator什么時候被繼續的異步動作能力。
### 提前完成
正如我們在本章早先講過的,連接到一個generator的迭代器支持可選的`return(..)`和`throw(..)`方法。它們倆都有立即中止一個暫停的的generator的效果。
考慮如下代碼:
```source-js
function *foo() {
yield 1;
yield 2;
yield 3;
}
var it = foo();
it.next(); // { value: 1, done: false }
it.return( 42 ); // { value: 42, done: true }
it.next(); // { value: undefined, done: true }
```
`return(x)`有點像強制一個`return x`就在那個時刻被處理,這樣你就立即得到這個指定的值。一旦一個generator完成,無論是正常地還是像展示的那樣提前地,它就不再處理任何代碼或返回任何值了。
`return(..)`除了可以手動調用,它還在迭代的最后被任何ES6中消費迭代器的結構自動調用,比如`for..of`循環和`...`擴散操作符。
這種能力的目的是,在控制端的代碼不再繼續迭代generator時它可以收到通知,這樣它就可能做一些清理工作(釋放資源,復位狀態,等等)。與普通函數的清理模式完全相同,達成這個目的的主要方法是使用一個`finally`子句:
```source-js
function *foo() {
try {
yield 1;
yield 2;
yield 3;
}
finally {
console.log( "cleanup!" );
}
}
for (var v of foo()) {
console.log( v );
}
// 1 2 3
// cleanup!
var it = foo();
it.next(); // { value: 1, done: false }
it.return( 42 ); // cleanup!
// { value: 42, done: true }
```
警告:?不要把`yield`語句放在`finally`子句內部!它是有效和合法的,但這確實是一個可怕的主意。它在某種意義上推遲了`return(..)`調用的完成,因為在`finally`子句中的任何`yield ..`表達式都被遵循來暫停和發送消息;你不會像期望的那樣立即得到一個完成的generator。基本上沒有任何好的理由去選擇這種瘋狂的?*壞的部分*,所以避免這么做!
前一個代碼段除了展示`return(..)`如何在中止generator的同時觸發`finally`子句,它還展示了一個generator在每次被調用時都產生一個全新的迭代器。事實上,你可以并發地使用連接到相同generator的多個迭代器:
```source-js
function *foo() {
yield 1;
yield 2;
yield 3;
}
var it1 = foo();
it1.next(); // { value: 1, done: false }
it1.next(); // { value: 2, done: false }
var it2 = foo();
it2.next(); // { value: 1, done: false }
it1.next(); // { value: 3, done: false }
it2.next(); // { value: 2, done: false }
it2.next(); // { value: 3, done: false }
it2.next(); // { value: undefined, done: true }
it1.next(); // { value: undefined, done: true }
```
#### 提前中止
你可以調用`throw(..)`來代替`return(..)`調用。就像`return(x)`實質上在generator當前的暫停點上注入了一個`return x`一樣,調用`throw(x)`實質上就像在暫停點上注入了一個`throw x`。
除了處理異常的行為(我們在下一節講解這對`try`子句意味著什么),`throw(..)`產生相同的提前完成 —— 在generator當前的暫停點中止它的運行。例如:
```source-js
function *foo() {
yield 1;
yield 2;
yield 3;
}
var it = foo();
it.next(); // { value: 1, done: false }
try {
it.throw( "Oops!" );
}
catch (err) {
console.log( err ); // Exception: Oops!
}
it.next(); // { value: undefined, done: true }
```
因為`throw(..)`基本上注入了一個`throw ..`來替換generator的`yield 1`這一行,而且沒有東西處理這個異常,它立即傳播回外面的調用端代碼,調用端代碼使用了一個`try..catch`來處理了它。
與`return(..)`不同的是,迭代器的`throw(..)`方法絕不會被自動調用。
當然,雖然沒有在前面的代碼段中展示,但如果當你調用`throw(..)`時有一個`try..finally`子句等在generator內部的話,這個`finally`子句將會在異常被傳播回調用端代碼之前有機會運行。
### 錯誤處理
正如我們已經得到的提示,generator中的錯誤處理可以使用`try..catch`表達,它在上行和下行兩個方向都可以工作。
```source-js
function *foo() {
try {
yield 1;
}
catch (err) {
console.log( err );
}
yield 2;
throw "Hello!";
}
var it = foo();
it.next(); // { value: 1, done: false }
try {
it.throw( "Hi!" ); // Hi!
// { value: 2, done: false }
it.next();
console.log( "never gets here" );
}
catch (err) {
console.log( err ); // Hello!
}
```
錯誤也可以通過`yield *`委托在兩個方向上傳播:
```source-js
function *foo() {
try {
yield 1;
}
catch (err) {
console.log( err );
}
yield 2;
throw "foo: e2";
}
function *bar() {
try {
yield *foo();
console.log( "never gets here" );
}
catch (err) {
console.log( err );
}
}
var it = bar();
try {
it.next(); // { value: 1, done: false }
it.throw( "e1" ); // e1
// { value: 2, done: false }
it.next(); // foo: e2
// { value: undefined, done: true }
}
catch (err) {
console.log( "never gets here" );
}
it.next(); // { value: undefined, done: true }
```
當`*foo()`調用`yield 1`時,值`1`原封不動地穿過了`*bar()`,就像我們已經看到過的那樣。
但這個代碼段最有趣的部分是,當`*foo()`調用`throw "foo: e2"`時,這個錯誤傳播到了`*bar()`并立即被`*bar()`的`try..catch`塊兒捕獲。錯誤沒有像值`1`那樣穿過`*bar()`。
然后`*bar()`的`catch`將`err`普通地輸出(`"foo: e2"`)之后`*bar()`就正常結束了,這就是為什么迭代器結果`{ value: undefined, done: true }`從`it.next()`中返回。
如果`*bar()`沒有用`try..catch`環繞著`yield *..`表達式,那么錯誤將理所當然地一直傳播出來,而且在它傳播的路徑上依然會完成(中止)`*bar()`。
### 轉譯一個Generator
有可能在ES6之前的環境中表達generator的能力嗎?事實上是可以的,而且有好幾種了不起的工具在這么做,包括最著名的Facebook的Regenerator工具 ([https://facebook.github.io/regenerator/)。](https://facebook.github.io/regenerator/)
但為了更好地理解generator,讓我們試著手動轉換一下。基本上講,我們將制造一個簡單的基于閉包的狀態機。
我們將使原本的generator非常簡單:
```source-js
function *foo() {
var x = yield 42;
console.log( x );
}
```
開始之前,我們將需要一個我們能夠執行的稱為`foo()`的函數,它需要返回一個迭代器:
```source-js
function foo() {
// ..
return {
next: function(v) {
// ..
}
// 我們將省略`return(..)`和`throw(..)`
};
}
```
現在,我們需要一些內部變量來持續跟蹤我們的“generator”的邏輯走到了哪一個步驟。我們稱它為`state`。我們將有三種狀態:起始狀態的`0`,等待完成`yield`表達式的`1`,和generator完成的`2`。
每次`next(..)`被調用時,我們需要處理下一個步驟,然后遞增`state`。為了方便,我們將每個步驟放在一個`switch`語句的`case`子句中,并且我們將它放在一個`next(..)`可以調用的稱為`nextState(..)`的內部函數中。另外,因為`x`是一個橫跨整個“generator”作用域的變量,所以它需要存活在`nextState(..)`函數的外部。
這是將它們放在一起(很明顯,為了使概念的展示更清晰,它經過了某些簡化):
```source-js
function foo() {
function nextState(v) {
switch (state) {
case 0:
state++;
// `yield`表達式
return 42;
case 1:
state++;
// `yield`表達式完成了
x = v;
console.log( x );
// 隱含的`return`
return undefined;
// 無需處理狀態`2`
}
}
var state = 0, x;
return {
next: function(v) {
var ret = nextState( v );
return { value: ret, done: (state == 2) };
}
// 我們將省略`return(..)`和`throw(..)`
};
}
```
最后,讓我們測試一下我們的前ES6“generator”:
```source-js
var it = foo();
it.next(); // { value: 42, done: false }
it.next( 10 ); // 10
// { value: undefined, done: true }
```
不賴吧?希望這個練習能在你的腦中鞏固這個概念:generator實際上只是狀態機邏輯的簡單語法。這使它們可以廣泛地應用。
### Generator的使用
我們現在非常深入地理解了generator如何工作,那么,它們在什么地方有用?
我們已經看過了兩種主要模式:
* *生產一系列值:*?這種用法可以很簡單(例如,隨機字符串或者遞增的數字),或者它也可以表達更加結構化的數據訪問(例如,迭代一個數據庫查詢結果的所有行)。
這兩種方式中,我們使用迭代器來控制generator,這樣就可以為每次`next(..)`調用執行一些邏輯。在數據解構上的普通迭代器只不過生成值而沒有任何控制邏輯。
* *串行執行的任務隊列:*?這種用法經常用來表達一個算法中步驟的流程控制,其中每一步都要求從某些外部數據源取得數據。對每塊兒數據的請求可能會立即滿足,或者可能會異步延遲地滿足。
從generator內部代碼的角度來看,在`yield`的地方,同步或異步的細節是完全不透明的。另外,這些細節被有意地抽象出去,如此就不會讓這樣的實現細節把各個步驟間自然的,順序的表達搞得模糊不清。抽象還意味著實現可以被替換/重構,而根本不用碰generator中的代碼。
當根據這些用法觀察generator時,它們的含義要比僅僅是手動狀態機的一種不同或更好的語法多多了。它們是一種用于組織和控制有序地生產與消費數據的強大工具。
## 模塊
我覺得這樣說并不夸張:在所有的JavaScript代碼組織模式中最重要的就是,而且一直是,模塊。對于我自己來說,而且我認為對廣大典型的技術社區來說,模塊模式驅動著絕大多數代碼。
### 過去的方式
傳統的模塊模式基于一個外部函數,它帶有內部變量和函數,以及一個被返回的“公有API”。這個“公有API”帶有對內部變量和功能擁有閉包的方法。它經常這樣表達:
```source-js
function Hello(name) {
function greeting() {
console.log( "Hello " + name + "!" );
}
// 公有API
return {
greeting: greeting
};
}
var me = Hello( "Kyle" );
me.greeting(); // Hello Kyle!
```
這個`Hello(..)`模塊通過被后續調用可以產生多個實例。有時,一個模塊為了作為一個單例(也就是,只需要一個實例)而只被調用一次,這樣的情況下常見的是一種前面代碼段的變種,使用IIFE:
```source-js
var me = (function Hello(name){
function greeting() {
console.log( "Hello " + name + "!" );
}
// 公有API
return {
greeting: greeting
};
})( "Kyle" );
me.greeting(); // Hello Kyle!
```
這種模式是經受過檢驗的。它也足夠靈活,以至于在許多不同的場景下可以有大量的各種變化。
其中一種最常見的是異步模塊定義(AMD),另一種是統一模塊定義(UMD)。我們不會在這里涵蓋這些特定的模式和技術,但是它們在網上的許多地方有大量的講解。
### 向前邁進
在ES6中,我們不再需要依賴外圍函數和閉包來為我們提供模塊支持了。ES6模塊擁有頭等語法上和功能上的支持。
在我們接觸這些具體語法之前,重要的是要理解ES6模塊與你以前曾經用過的模塊比較起來,在概念上的一些相當顯著的不同之處:
* ES6使用基于文件的模塊,這意味著一個模塊一個文件。目前,沒有標準的方法將多個模塊組合到一個文件中。
這意味著如果你要直接把ES6模塊加載到一個瀏覽器web應用中的話,你將個別地加載它們,不是像常見的那樣為了性能優化而作為一個單獨文件中的一個巨大的包加載。
預計同時期到來的HTTP/2將會大幅緩和這種性能上的顧慮,因為它工作在一個持續的套接字連接上,因而可以用并行的,互相交錯的方式非常高效地加載許多小文件。
* 一個ES6模塊的API是靜態的。這就是說,你在模塊的公有API上靜態地定義所有被導出的頂層內容,而這些內容導出之后不能被修改。
有些用法習慣于能夠提供動態API定義,它的方法可以根據運行時的條件被增加/刪除/替換。這些用法要么必須改變以適應ES6靜態API,要么它們就不得不將屬性/方法的動態修改限制在一個內層對象中。
* ES6模塊都是單例。也就是,模塊只有一個維持它狀態的實例。每次你將這個模塊導入到另一個模塊時,你得到的都是一個指向中央實例的引用。如果你想要能夠產生多個模塊實例,你的模塊將需要提供某種工廠來這么做。
* 你在模塊的公有API上暴露的屬性和方法不是值和引用的普通賦值。它們是在你內部模塊定義中的標識符的實際綁定(幾乎就是指針)。
在前ES6的模塊中,如果你將一個持有像數字或者字符串這樣基本類型的屬性放在你的共有API中,那么這個屬性是通過值拷貝賦值的,任何對相應內部變量的更新都將是分離的,不會影響在API對象上的共有拷貝。
在ES6中,導出一個本地私有變量,即便它當前持有一個基本類型的字符串/數字/等等,導出的都是這個變量的一個綁定。如果這個模塊改變了這個變量的值,外部導入的綁定就會解析為那個新的值。
* 導入一個模塊和靜態地請求它被加載是同一件事情(如果它還沒被加載的話)。如果你在瀏覽器中,這意味著通過網絡的阻塞加載。如果你在服務器中,它是一個通過文件系統的阻塞加載。
但是,不要對它在性能的影響上驚慌。因為ES6模塊是靜態定義的,導入的請求可以被靜態地掃描,并提前加載,甚至是在你使用這個模塊之前。
ES6并沒有實際規定或操縱這些加載請求如何工作的機制。有一個模塊加載器的分離概念,它讓每一個宿主環境(瀏覽器,Node.js,等等)為該環境提供合適的默認加載器。一個模塊的導入使用一個字符串值來表示從哪里去取得模塊(URL,文件路徑,等等),但是這個值在你的程序中是不透明的,它僅對加載器自身有意義。
如果你想要比默認加載器提供的更細致的控制能力,你可以定義你自己的加載器 —— 默認加載器基本上不提供任何控制,它對于你的程序代碼是完全隱藏的。
如你所見,ES6模塊將通過封裝,控制共有API,以及應用依賴導入來服務于所有的代碼組織需求。但是它們用一種非常特別的方式來這樣做,這可能與你已經使用多年的模塊方式十分接近,也肯能差得很遠。
#### CommonJS
有一種相似,但不是完全兼容的模塊語法,稱為CommonJS,那些使用Node.js生態系統的人很熟悉它。
不太委婉地說,從長久看來,ES6模塊實質上將要取代所有先前的模塊格式與標準,即便是CommonJS,因為它們是建立在語言的語法支持上的。如果除了普遍性以外沒有其他原因,遲早ES6將不可避免地作為更好的方式勝出。
但是,要達到那一天我們還有相當長的路要走。在服務器端的JavaScript世界中差不多有成百上千的CommonJS風格模塊,而在瀏覽器的世界里各種格式標準的模塊(UMD,AMD,臨時性的模塊方案)數量還要多十倍。這要花許多年過渡才能取得任何顯著的進展。
在這個過渡期間,模塊轉譯器/轉換器將是絕對必要的。你可能剛剛適應了這種新的現實。不論你是使用正規的模塊,AMD,UMD,CommonJS,或者ES6,這些工具都不得不解析并轉換為適合你代碼運行環境的格式。
對于Node.js,這可能意味著(目前)轉換的目標是CommonJS。對于瀏覽器來說,可能是UMD或者AMD。除了在接下來的幾年中隨著這些工具的成熟和最佳實踐的出現而發生的許多變化。
從現在起,我能對模塊的提出的最佳建議是:不管你曾經由于強烈的愛好而虔誠地追隨哪一種格式,都要培養對理解ES6模塊的欣賞能力,并讓你對其他模塊模式的傾向性漸漸消失掉。它們就是JS中模塊的未來,即便現實有些偏差。
### 新的方式
使用ES6模塊的兩個主要的新關鍵字是`import`和`export`。在語法上有許多微妙的地方,那么讓我們深入地看看。
警告:?一個容易忽視的重要細節:`import`和`export`都必須總是出現在它們分別被使用之處的頂層作用域。例如,你不能把`import`或`export`放在一個`if`條件內部;它們必須出現在所有塊兒和函數的外部。
#### `export`API成員
`export`關鍵字要么放在一個聲明的前面,要么就與一組特殊的要被導出的綁定一起用作一個操作符。考慮如下代碼:
```source-js
export function foo() {
// ..
}
export var awesome = 42;
var bar = [1,2,3];
export { bar };
```
表達相同導出的另一種方法:
```source-js
function foo() {
// ..
}
var awesome = 42;
var bar = [1,2,3];
export { foo, awesome, bar };
```
這些都稱為?*命名導出*,因為你實際上導出的是變量/函數/等等其他的名稱綁定。
任何你沒有使用`export`*標記*?的東西將在模塊作用域的內部保持私有。也就是說,雖然有些像`var bar = ..`的東西看起來像是在頂層全局作用域中聲明的,但是這個頂層作用域實際上是模塊本身;在模塊中沒有全局作用域。
注意:?模塊確實依然可以訪問掛在它外面的`window`和所有的“全局”,只是不作為頂層詞法作用域而已。但是,你真的應該在你的模塊中盡可能地遠離全局。
你還可以在命名導出期間“重命名”(也叫別名)一個模塊成員:
```source-js
function foo() { .. }
export { foo as bar };
```
當這個模塊被導入時,只有成員名稱`bar`可以用于導入;`foo`在模塊內部保持隱藏。
模塊導出不像你習以為常的`=`賦值操作符那樣,僅僅是值或引用的普通賦值。實際上,當你導出某些東西時,你導出了一個對那個東西(變量等)的一個綁定(有些像指針)。
在你的模塊內部,如果你改變一個你已經被導出綁定的變量的值,即使它已經被導入了(見下一節),這個被導入的綁定也將解析為當前的(更新后的)值。
考慮如下代碼:
```source-js
var awesome = 42;
export { awesome };
// 稍后
awesome = 100;
```
當這個模塊被導入時,無論它是在`awesome = 100`設定的之前還是之后,一旦這個賦值發生,被導入的綁定都將被解析為值`100`,不是`42`。
這是因為,這個綁定實質上是一個指向變量`awesome`本身的一個引用,或指針,而不是它的值的一個拷貝。ES6模塊綁定引入了一個對于JS來說幾乎是史無前例的概念。
雖然你顯然可以在一個模塊定義的內部多次使用`export`,但是ES6絕對偏向于一個模塊只有一個單獨導出的方式,這稱為?*默認導出*。用TC39協會的一些成員的話說,如果你遵循這個模式你就可以“獲得更簡單的`import`語法作為獎勵”,如果你不遵循你就會反過來得到更繁冗的語法作為“懲罰”。
一個默認導出將一個特定的導出綁定設置為在這個模塊被導入時的默認綁定。這個綁定的名稱是字面上的`default`。正如你即將看到的,在導入模塊綁定時你還可以重命名它們,你經常會對默認導出這么做。
每個模塊定義只能有一個`default`。我們將在下一節中講解`import`,你將看到如果模塊擁有默認導入時`import`語法如何變得更簡潔。
默認導出語法有一個微妙的細節你應當多加注意。比較這兩個代碼段:
```source-js
function foo(..) {
// ..
}
export default foo;
```
和這一個:
```source-js
function foo(..) {
// ..
}
export { foo as default };
```
在第一個代碼段中,你導出的是那一個函數表達式在那一刻的值的綁定,*不是*?標識符`foo`的綁定。換句話說,`export default ..`接收一個表達式。如果你稍后在你的模塊內部賦給`foo`一個不同的值,這個模塊導入將依然表示原本被導出的函數,而不是那個新的值。
順帶一提,第一個代碼段還可以寫做:
```source-js
export default function foo(..) {
// ..
}
```
警告:?雖然技術上講這里的`function foo..`部分是一個函數表達式,但是對于模塊內部作用域來說,它被視為一個函數聲明,因為名稱`foo`被綁定在模塊的頂層作用域(經常稱為“提升”)。對`export default var foo = ..`也是如此。然而,雖然你?*可以*`export var foo = ..`,但是一個令人沮喪的不一致是,你目前還不能`export default bar foo = ..`(或者`let`和`const`)。在寫作本書時,為了保持一致性,已經開始了在后ES6不久的時期增加這種能力的討論。
再次回想一下第二個代碼段:
```source-js
function foo(..) {
// ..
}
export { foo as default };
```
這種版本的模塊導出中,默認導出的綁定實際上是標識符`foo`而不是它的值,所以你會得到先前描述過的綁定行為(也就是,如果你稍后改變`foo`的值,在導入一端看到的值也會被更新)。
要非常小心這種默認導出語法的微妙區別,特別是在你的邏輯需要導出的值要被更新時。如果你永遠不打算更新一個默認導出的值,`export default ..`就沒問題。如果你確實打算更新這個值,你必須使用`export { .. as default }`。無論哪種情況,都要確保注釋你的代碼以解釋你的意圖!
因為一個模塊只能有一個`default`,這可能會誘使你將你的模塊設計為默認導出一個帶有你所有API方法的對象,就像這樣:
```source-js
export default {
foo() { .. },
bar() { .. },
..
};
```
這種模式看起來十分接近于許多開發者構建它們的前ES6模塊時曾經用過的模式,所以它看起來像是一種十分自然的方式。不幸的是,它有一些缺陷并且不為官方所鼓勵使用。
特別是,JS引擎不能靜態地分析一個普通對象的內容,這意味著它不能為靜態`import`性能進行一些優化。使每個成員獨立地并明確地導出的好處是,引擎?*可以*?進行靜態分析和性能優化。
如果你的API已經有多于一個的成員,這些原則 —— 一個模塊一個默認導出,和所有API成員作為被命名的導出 —— 看起來是沖突的,不是嗎?但是你?*可以*?有一個單獨的默認導出并且有其他的被命名導出;它們不是互相排斥的。
所以,取代這種(不被鼓勵使用的)模式:
```source-js
export default function foo() { .. }
foo.bar = function() { .. };
foo.baz = function() { .. };
```
你可以這樣做:
```source-js
export default function foo() { .. }
export function bar() { .. }
export function baz() { .. }
```
注意:?在前面這個代碼段中,我為標記為`default`的函數使用了名稱`foo`。但是,這個名稱`foo`為了導出的目的而被忽略掉了 ——?`default`才是實際上被導出的名稱。當你導入這個默認綁定時,你可以叫它任何你想用的名字,就像你將在下一節中看到的。
或者,一些人喜歡:
```source-js
function foo() { .. }
function bar() { .. }
function baz() { .. }
export { foo as default, bar, baz, .. };
```
混合默認和被命名導出的效果將在稍后我們講解`import`時更加清晰。但它實質上意味著最簡潔的默認導入形式將僅僅取回`foo()`函數。用戶可以額外地手動羅列`bar`和`baz`作為命名導入,如果他們想用它們的話。
你可能能夠想象,如果你的模塊有許多命名導出綁定,那么對于模塊的消費者來說將有多么乏味。有一個通配符導入形式,你可以在一個名稱空間對象中導入一個模塊的所有導出,但是沒有辦法用通配符導入到頂層綁定。
要重申的是,ES6模塊機制被有意設計為不鼓勵帶有許多導出的模塊;相對而言,它被期望成為一種更困難一些的,作為某種社會工程的方式,以鼓勵對大型/復雜模塊設計有利的簡單模塊設計。
我將可能推薦你不要將默認導出與命名導出混在一起,特別是當你有一個大型API,并且將它重構為分離的模塊是不現實或不希望的時候。在這種情況下,就都使用命名導出,并在文檔中記錄你的模塊的消費者可能應當使用`import * as ..`(名稱空間導入,在下一節中討論)方式來將整個API一次性地帶到一個單獨的名稱空間中。
我們早先提到過這一點,但讓我們回過頭來更詳細地討論一下。除了導出一個表達式的值的綁定的`export default ...`形式,所有其他的導出形式都導出本地標識符的綁定。對于這些綁定,如果你在導出之后改變一個模塊內部變量的值,外部被導入的綁定將可以訪問這個被更新的值:
```source-js
var foo = 42;
export { foo as default };
export var bar = "hello world";
foo = 10;
bar = "cool";
```
當你導出這個模塊時,`default`和`bar`導出將會綁定到本地變量`foo`和`bar`,這意味著它們將反映被更新的值`10`和`"cool"`。在被導出時的值是無關緊要的。在被導入時的值是無關緊要的。這些綁定是實時的鏈接,所以唯一重要的是當你訪問這個綁定時它當前的值是什么。
警告:?雙向綁定是不允許的。如果你從一個模塊中導入一個`foo`,并試圖改變你導入的變量`foo`的值,一個錯誤就會被拋出!我們將在下一節重新回到這個問題。
你還可以重新導出另一個模塊的導出,比如:
```source-js
export { foo, bar } from "baz";
export { foo as FOO, bar as BAR } from "baz";
export * from "baz";
```
這些形式都與首先從`"baz"`模塊導入然后為了從你的模塊中到處而明確地羅列它的成員相似。然而,在這些形式中,模塊`"baz"`的成員從沒有被導入到你的模塊的本地作用域;某種程度上,它們原封不動地穿了過去。
#### `import`API成員
要導入一個模塊,你將不出意料地使用`import`語句。就像`export`有幾種微妙的變化一樣,`import`也有,所以你要花相當多的時間來考慮下面的問題,并試驗你的選擇。
如果你想要導入一個模塊的API中的特定命名成員到你的頂層作用域,使用這種語法:
```source-js
import { foo, bar, baz } from "foo";
```
警告:?這里的`{ .. }`語法可能看起來像一個對象字面量,甚至是像一個對象解構語法。但是,它的形式僅對模塊而言是特殊的,所以不要將它與其他地方的`{ .. }`模式搞混了。
字符串`"foo"`稱為一個?*模塊指示符*。因為它的全部目的在于可以靜態分析的語法,所以模塊指示符必須是一個字符串字面量;它不能是一個持有字符串值的變量。
從你的ES6代碼和JS引擎本身的角度來看,這個字符串字面量的內容是完全不透明和沒有意義的。模塊加載器將會把這個字符串翻譯為一個在何處尋找被期望的模塊的指令,不是作為一個URL路徑就是一個本地文件系統路徑。
被羅列的標識符`foo`,`bar`和`baz`必須匹配在模塊的API上的命名導出(這里將會發生靜態分析和錯誤斷言)。它們在你當前的作用域中被綁定為頂層標識符。
```source-js
import { foo } from "foo";
foo();
```
你可以重命名被導入的綁定標識符,就像:
```source-js
import { foo as theFooFunc } from "foo";
theFooFunc();
```
如果這個模塊僅有一個你想要導入并綁定到一個標識符的默認導出,你可以為這個綁定選擇性地跳過外圍的`{ .. }`語法。在這種首選情況下`import`會得到最好的最簡潔的`import`語法形式:
```source-js
import foo from "foo";
// 或者:
import { default as foo } from "foo";
```
注意:?正如我們在前一節中講解過的,一個模塊的`export`中的`default`關鍵字指定了一個名稱實際上為`default`的命名導出,正如在第二個更加繁冗的語法中展示的那樣。在這個例子中,從`default`到`foo`的重命名在后者的語法中是明確的,并且與前者隱含地重命名是完全相同的。
如果模塊有這樣的定義,你還可以與其他的命名導出一起導入一個默認導出。回憶一下先前的這個模塊定義:
```source-js
export default function foo() { .. }
export function bar() { .. }
export function baz() { .. }
```
要引入這個模塊的默認導出和它的兩個命名導出:
```source-js
import FOOFN, { bar, baz as BAZ } from "foo";
FOOFN();
bar();
BAZ();
```
ES6的模塊哲學強烈推薦的方式是,你只從一個模塊中導入你需要的特定的綁定。如果一個模塊提供10個API方法,但是你只需它們中的兩個,有些人認為帶入整套API綁定是一種浪費。
一個好處是,除了代碼變得更加明確,收窄導入使得靜態分析和錯誤檢測(例如,不小心使用了錯誤的綁定名稱)變得更加健壯。
當然,這只是受ES6設計哲學影響的標準觀點;沒有什么東西要求我們堅持這種方式。
許多開發者可能很快指出這樣的方式更令人厭煩,每次你發現自己需要一個模塊中的其他某些東西時,它要求你經常地重新找到并更新你的`import`語句。它的代價是犧牲便利性。
以這種觀點看,首選方式可能是將模塊中的所有東西都導入到一個單獨的名稱空間中,而不是將每個個別的成員直接導入到作用域中。幸運的是,`import`語句擁有一個變種語法可以支持這種風格的模塊使用,它被稱為?*名稱空間導入*。
考慮一個被這樣導出的`"foo"`模塊:
```source-js
export function bar() { .. }
export var x = 42;
export function baz() { .. }
```
你可以將整個API導入到一個單獨的模塊名稱空間綁定中:
```source-js
import * as foo from "foo";
foo.bar();
foo.x; // 42
foo.baz();
```
注意:?`* as ..`子句要求使用`*`通配符。換句話說,你不能做像`import { bar, x } as foo from "foo"`這樣的事情來將API的一部分綁定到`foo`名稱空間。我會很喜歡這樣的東西,但是對ES6的名稱空間導入來說,要么全有要么全無。
如果你正在使用`* as ..`導入的模塊擁有一個默認導出,它會在指定的名稱空間中被命名為`default`。你可以在這個名稱空間綁定的外面,作為一個頂層標識符額外地命名這個默認導出。考慮一個被這樣導出的`"world"`模塊:
```source-js
export default function foo() { .. }
export function bar() { .. }
export function baz() { .. }
```
和這個`import`:
```source-js
import foofn, * as hello from "world";
foofn();
hello.default();
hello.bar();
hello.baz();
```
雖然這個語法是合法的,但是它可能令人困惑:這個模塊的一個方法(那個默認導出)被綁定到你作用域的頂層,然而其他的命名導出(而且之中之一稱為`default`)作為一個不同名稱(`hello`)的標識符名稱空間的屬性被綁定。
正如我早先提到的,我的建議是避免這樣設計你的模塊導出,以降低你模塊的用戶受困于這些奇異之處的可能性。
所有被導入的綁定都是不可變和/或只讀的。考慮前面的導入;所有這些后續的賦值嘗試都將拋出`TypeError`:
```source-js
import foofn, * as hello from "world";
foofn = 42; // (運行時)TypeError!
hello.default = 42; // (運行時)TypeError!
hello.bar = 42; // (運行時)TypeError!
hello.baz = 42; // (運行時)TypeError!
```
回憶早先在“`export`?API成員”一節中,我們談到`bar`和`baz`綁定是如何被綁定到`"world"`模塊內部的實際標識符上的。它意味著如果模塊改變那些值,`hello.bar`和`hello.baz`將引用更新后的值。
但是你的本地導入綁定的不可變/只讀的性質強制你不能從被導入的綁定一方改變他們,不然就會發生`TypeError`。這很重要,因為如果沒有這種保護,你的修改將會最終影響所有其他該模塊的消費者(記住:單例),這可能會產生一些非常令人吃驚的副作用!
另外,雖然一個模塊?*可以*?從內部改變它的API成員,但你應當對有意地以這種風格設計你的模塊非常謹慎。ES6模塊?*被預計*?是靜態的,所以背離這個原則應當是不常見的,而且應當在文檔中被非常小心和詳細地記錄下來。
警告:?存在一些這樣的模塊設計思想,你實際上打算允許一個消費者改變你的API上的一個屬性的值,或者模塊的API被設計為可以通過向API的名稱空間中添加“插件”來“擴展”。但正如我們剛剛斷言的,ES6模塊API應當被認為并設計為靜態的和不可變的,這強烈地約束和不鼓勵那些其他的模塊設計模式。你可以通過導出一個普通對象 —— 它理所當然是可以隨意改變的 —— 來繞過這些限制。但是在選擇這條路之前要三思而后行。
作為一個`import`的結果發生的聲明將被“提升”(參見本系列的?*作用域與閉包*)。考慮如下代碼:
```source-js
foo();
import { foo } from "foo";
```
`foo()`可以運行是因為`import ..`語句的靜態解析不僅在編譯時搞清了`foo`是什么,它還將這個聲明“提升”到模塊作用域的頂部,如此使它在模塊中通篇都是可用的。
最后,最基本的`import`形式看起來像這樣:
```source-js
import "foo";
```
這種形式實際上不會將模塊的任何綁定導入到你的作用域中。它加載(如果還沒被加載過),編譯(如果還沒被編譯過),并對`"foo"`模塊求值(如果還沒被運行過)。
一般來說,這種導入可能不會特別有用。可能會有一些模塊的定義擁有副作用(比如向`window`/全局對象賦值)的特殊情況。你還可以將`import "foo"`用作稍后可能需要的模塊的預加載。
### 模塊循環依賴
A導入B。B導入A。這將如何工作?
我要立即聲明,一般來說我會避免使用刻意的循環依賴來設計系統。話雖如此,我也認識到人們這么做是有原因的,而且它可以解決一些艱難的設計問題。
讓我們考慮一下ES6如何處理這種情況。首先,模塊`"A"`:
```source-js
import bar from "B";
export default function foo(x) {
if (x > 10) return bar( x - 1 );
return x * 2;
}
```
現在,是模塊`"B"`:
```source-js
import foo from "A";
export default function bar(y) {
if (y > 5) return foo( y / 2 );
return y * 3;
}
```
這兩個函數,`foo(..)`和`bar(..)`,如果它們在相同的作用域中就會像標準的函數聲明那樣工作,因為聲明被“提升”至整個作用域,而因此與它們的編寫順序無關,它們互相是可用的。
在模塊中,你的聲明在完全不同的作用域中,所以ES6必須做一些額外的工作以使這些循環引用工作起來。
在大致的概念上,這就是循環的`import`依賴如何被驗證和解析的:
* 如果模塊`"A"`被首先加載,第一步將是掃描文件并分析所有的導出,這樣就可以為導入注冊所有可用的綁定。然后它處理`import .. from "B"`,這指示它需要去取得`"B"`。
* 一旦引擎加載了`"B"`,它會做同樣的導出綁定分析。當它看到`import .. from "A"`時,它知道`"A"`的API已經準備好了,所以它可以驗證這個`import`為合法的。現在它知道了`"B"`的API,它也可以驗證在模塊`"A"`中等待的`import .. from "B"`了。
實質上,這種相互導入,連同對兩個`import`語句合法性的靜態驗證,虛擬地組合了兩個分離的模塊作用域(通過綁定),因此`foo(..)`可以調用`bar(..)`或相反。這與我們在相同的作用域中聲明是對稱的。
現在讓我們試著一起使用這兩個模塊。首先,我們將試用`foo(..)`:
```source-js
import foo from "foo";
foo( 25 ); // 11
```
或者我們可以試用`bar(..)`:
```source-js
import bar from "bar";
bar( 25 ); // 11.5
```
在`foo(25)`調用`bar(25)`被執行的時刻,所有模塊的所有分析/編譯都已經完成了。這意味著`foo(..)`內部地直接知道`bar(..)`,而且`bar(..)`內部地直接知道`foo(..)`。
如果所有我們需要的僅是與`foo(..)`互動,那么我們只需要導入`"foo"`模塊。`bar(..)`和`"bar"`模塊也同理。
當然,如果我們想,我們?*可以*?導入并使用它們兩個:
```source-js
import foo from "foo";
import bar from "bar";
foo( 25 ); // 11
bar( 25 ); // 11.5
```
`import`語句的靜態加載語義意味著通過`import`互相依賴對方的`"foo"`和`"bar"`將確保在它們運行前被加載,解析,和編譯。所以它們的循環依賴是被靜態地解析的,而且將會如你所愿地工作。
### 模塊加載
我們在“模塊”這一節的最開始聲稱,`import`語句使用了一個由宿主環境(瀏覽器,Node.js,等等)提供的分離的機制,來實際地將模塊指示符字符串解析為一些對尋找和加載所期望模塊的有用的指令。這種機制就是系統?*模塊加載器*。
由環境提供的默認模塊加載器,如果是在瀏覽器中將會把模塊指示符解釋為一個URL,如果是在服務器端(一般地)將會解釋為一個本地文件系統路徑,比如Node.js。它的默認行為是假定被加載的文件是以ES6標準的模塊格式編寫的。
另外,與當下腳本程序被加載的方式相似,你將可以通過一個HTML標簽將一個模塊加載到瀏覽器中。在本書寫作時,這個標簽將會是`<script type="module">`還是`<module>`還不完全清楚。ES6沒有控制這個決定,但是在相應的標準化機構中的討論早已隨著ES6開始了。
無論這個標簽看起來什么樣,你可以確信它的內部將會使用默認加載器(或者一個你預先指定好的加載器,就像我們將在下一節中討論的)。
就像你將在標記中使用的標簽一樣,ES6沒有規定模塊加載器本身。它是一個分離的,目前由WHATWG瀏覽器標準化小組控制的平行的標準。([http://whatwg.github.io/loader/](http://whatwg.github.io/loader/))
在本書寫作時,接下來的討論反映了它的API設計的一個早期版本,和一些可能將要改變的東西。
#### 加載模塊之外的模塊
一個與模塊加載器直接交互的用法,是當一個非模塊需要加載一個模塊時。考慮如下代碼:
```source-js
// 在瀏覽器中通過`<script>`加載的普通script,
// `import`在這里是不合法的
Reflect.Loader.import( "foo" ) // 返回一個`"foo"`的promise
.then( function(foo){
foo.bar();
} );
```
工具`Reflect.Loader.import(..)`將整個模塊導入到命名參數中(作為一個名稱空間),就像我們早先討論過的`import * as foo ..`名稱空間導入。
注意:?`Reflect.Loader.import(..)`返回一個promise,它在模塊準備好時被完成。要導入多個模塊的話,你可以使用`Promise.all([ .. ])`將多個`Reflect.Loader.import(..)`的promise組合起來。有關Promise的更多信息,參見第四章的“Promise”。
你還可以在一個真正的模塊中使用`Reflect.Loader.import(..)`來動態地/條件性地加載一個模塊,這是`import`自身無法做到的。例如,你可能在一個特性測試表明某個ES7+特性沒有被當前的引擎所定義的情況下,選擇性地加載一個含有此特性的填補的模塊。
由于性能的原因,你將想要盡量避免動態加載,因為它阻礙了JS引擎從它的靜態分析中提前獲取的能力。
#### 自定義加載
直接與模塊加載器交互的另外一種用法是,你想要通過配置或者甚至是重定義來定制它的行為。
在本書寫作時,有一個被開發好的模塊加載器API的填補([https://github.com/ModuleLoader/es6-module-loader)。雖然關于它的細節非常匱乏,而且很可能改變,但是我們可以通過它來探索最終可能固定下來的東西是什么。](https://github.com/ModuleLoader/es6-module-loader)
`Reflect.Loader.import(..)`調用可能會支持第二個參數,它指定各種選項來定制導入/加載任務。例如:
```source-js
Reflect.Loader.import( "foo", { address: "/path/to/foo.js" } )
.then( function(foo){
// ..
} )
```
還有一種預期是,會為一個自定義內容提供某種機制來將之掛鉤到模塊加載的處理過程中,就在翻譯/轉譯可能發生的加載之后,但是在引擎編譯這個模塊之前。
例如,你可能會加載某些還不是ES6兼容的模塊格式的東西(例如,CoffeeScript,TypeScript,CommonJS,AMD)。你的翻譯步驟可能會為了后面的引擎處理而將它轉換為ES6兼容的模塊。
## 類
幾乎從JavaScript的最開始的那時候起,語法和開發模式都曾努力(讀作:掙扎地)地戴上一個支持面向類的開發的假面具。伴隨著`new`和`instanceof`和一個`.constructor`屬性,誰能不認為JS在它的原型系統的某個地方藏著類機制呢?
當然,JS的“類”與經典的類完全不同。其區別有很好的文檔記錄,所以在此我不會在這一點上花更多力氣。
注意:?要學習更多關于在JS中假冒“類”的模式,以及另一種稱為“委托”的原型的視角,參見本系列的?*this與對象原型*?的后半部分。
### `class`
雖然JS的原型機制與傳統的類的工作方式不同,但是這并不能阻擋一種強烈的潮流 —— 要求這門語言擴展它的語法糖以便將“類”表達得更像真正的類。讓我們進入ES6`class`關鍵字和它相關的機制。
這個特性是一個具有高度爭議、曠日持久的爭論的結果,而且代表了幾種對關于如何處理JS類的強烈反對意見的妥協的一小部分。大多數希望JS擁有完整的類機制的開發者將會發現新語法的一些部分十分吸引人,但是也會發現一些重要的部分仍然缺失了。但不要擔心,TC39已經致力于另外的特性,以求在后ES6時代中增強類機制。
新的ES6類機制的核心是`class`關鍵字,它標識了一個?*塊*,其內容定義了一個函數的原型的成員。考慮如下代碼:
```source-js
class Foo {
constructor(a,b) {
this.x = a;
this.y = b;
}
gimmeXY() {
return this.x * this.y;
}
}
```
一些要注意的事情:
* `class Foo`?暗示著創建一個(特殊的)名為`Foo`的函數,與你在前ES6中所做的非常相似。
* `constructor(..)`表示了這個`Foo(..)`函數的簽名,和它的函數體內容。
* 類方法同樣使用對象字面量中可以使用的“簡約方法”語法,正如在第二章中討論過的。這也包括在本章早先討論過的簡約generator,以及ES5的getter/setter語法。但是,類方法是不可枚舉的而對象方法默認是可枚舉的。
* 與對象字面量不同的是,在一個`class`內容的部分沒有逗號分隔各個成員!事實上,這甚至是不允許的。
前一個代碼段的`class`語法定義可以大致認為和這個前ES6等價物相同,對于那些以前做過原型風格代碼的人來說可能十分熟悉它:
```source-js
function Foo(a,b) {
this.x = a;
this.y = b;
}
Foo.prototype.gimmeXY = function() {
return this.x * this.y;
}
```
不管是前ES6形式還是新的ES6`class`形式,這個“類”現在可以被實例化并如你所想地使用了:
```source-js
var f = new Foo( 5, 15 );
f.x; // 5
f.y; // 15
f.gimmeXY(); // 75
```
注意!雖然`class Foo`看起來很像`function Foo()`,但是有一些重要的區別:
* `class Foo`的一個`Foo(..)`調用?*必須*?與`new`一起使用,因為前ES6的`Foo.call( obj )`方式?*不能*?工作。
* 雖然`function Foo`會被“提升”(參見本系列的?*作用域與閉包*),但是`class Foo`不會;`extends ..`指定的表達式不能被“提升”。所以,在你能夠實例化一個`class`之前必須先聲明它。
* 在頂層全局作用域中的`class Foo`在這個作用域中創建了一個詞法標識符`Foo`,但與此不同的是`function Foo`不會創建一個同名的全局對象屬性。
已經建立的`instanceof`操作仍然可以與ES6的類一起工作,因為`class`只是創建了一個同名的構造器函數。然而,ES6引入了一個定制`instanceof`如何工作的方法,使用`Symbol.hasInstance`(參見第七章的“通用Symbol”)。
我發現另一種更方便地考慮`class`的方法是,將它作為一個用來自動填充`proptotype`對象的?*宏*。可選的是,如果使用`extends`(參見下一節)的話它還能連接`[[Prototype]]`關系。
其實一個ES6`class`本身不是一個實體,而是一個元概念,它包裹在其他具體實體上,例如函數和屬性,并將它們綁在一起。
提示:?除了這種聲明的形式,一個`class`還可以是一個表達式,就像:`var x = class Y { .. }`。這主要用于將類的定義(技術上說,是構造器本身)作為函數參數值傳遞,或者將它賦值給一個對象屬性。
### `extends`?和?`super`
ES6的類還有一種語法糖,用于在兩個函數原型之間建立`[[Prototype]]`委托鏈 —— 通常被錯誤地標記為“繼承”或者令人困惑地標記為“原型繼承” —— 使用我們熟悉的面向類的術語`extends`:
```source-js
class Bar extends Foo {
constructor(a,b,c) {
super( a, b );
this.z = c;
}
gimmeXYZ() {
return super.gimmeXY() * this.z;
}
}
var b = new Bar( 5, 15, 25 );
b.x; // 5
b.y; // 15
b.z; // 25
b.gimmeXYZ(); // 1875
```
一個有重要意義的新增物是`super`,它實際上在前ES6中不是直接可能的東西(不付出一些不幸的黑科技的代價的話)。在構造器中,`super`自動指向“父構造器”,這在前一個例子中是`Foo(..)`。在方法中,它指向“父對象”,如此你就可以訪問它上面的屬性/方法,比如`super.gimmeXY()`。
`Bar extends Foo`理所當然地意味著將`Bar.prototype`的`[[Prototype]]`鏈接到`Foo.prototype`。所以,在`gimmeXYZ()`這樣的方法中的`super`特被地意味著`Foo.prototype`,而當`super`用在`Bar`構造器中時意味著`Foo`。
注意:?`super`不僅限于`class`聲明。它也可以在對象字面量中工作,其方式在很大程度上與我們在此討論的相同。更多信息參見第二章中的“對象`super`”。
#### `super`的坑
注意到`super`的行為根據它出現的位置不同而不同是很重要的。公平地說,大多數時候這不是一個問題。但是如果你背離一個狹窄的規范,令人詫異的事情就會等著你。
可能會有這樣的情況,你想在構造器中引用`Foo.prototype`,比如直接訪問它的屬性/方法之一。然而,在構造器中的`super`不能這樣被使用;`super.prototype`將不會工作。`super(..)`大致上意味著調用`new Foo(..)`,但它實際上不是一個可用的對`Foo`本身的引用。
與此對稱的是,你可能想要在一個非構造器方法中引用`Foo(..)`函數。`super.constructor`將會指向`Foo(..)`函數,但是要小心這個函數?*只能*?與`new`一起被調用。`new super.constructor(..)`將是合法的,但是在大多數情況下它都不是很有用, 因為你不能使這個調用使用或引用當前的`this`對象環境,而這很可能是你想要的。
另外,`super`看起來可能就像`this`一樣是被函數的環境所驅動的 —— 也就是說,它們都是被動態綁定的。但是,`super`不像`this`那樣是動態的。當聲明時一個構造器或者方法在它內部使用一個`super`引用時(在`class`的內容部分),這個`super`是被靜態地綁定到這個指定的類階層中的,而且不能被覆蓋(至少是在ES6中)。
這意味著什么?這意味著如果你習慣于從一個“類”中拿來一個方法并通過覆蓋它的`this`,比如使用`call(..)`或者`apply(..)`,來為另一個類而“借用”它的話,那么當你借用的方法中有一個`super`時,將很有可能發生令你詫異的事情。考慮這個類階層:
```source-js
class ParentA {
constructor() { this.id = "a"; }
foo() { console.log( "ParentA:", this.id ); }
}
class ParentB {
constructor() { this.id = "b"; }
foo() { console.log( "ParentB:", this.id ); }
}
class ChildA extends ParentA {
foo() {
super.foo();
console.log( "ChildA:", this.id );
}
}
class ChildB extends ParentB {
foo() {
super.foo();
console.log( "ChildB:", this.id );
}
}
var a = new ChildA();
a.foo(); // ParentA: a
// ChildA: a
var b = new ChildB(); // ParentB: b
b.foo(); // ChildB: b
```
在前面這個代碼段中一切看起來都相當自然和在意料之中。但是,如果你試著借來`b.foo()`并在`a`的上下文中使用它的話 —— 通過動態`this`綁定的力量,這樣的借用十分常見而且以許多不同的方式被使用,包括最明顯的mixin —— 你可能會發現這個結果出奇地難看:
```source-js
// 在`a`的上下文環境中借用`b.foo()`
b.foo.call( a ); // ParentB: a
// ChildB: a
```
如你所見,引用`this.id`被動態地重綁定所以在兩種情況下都報告`: a`而不是`: b`。但是`b.foo()`的`super.foo()`引用沒有被動態重綁定,所以它依然報告`ParentB`而不是期望的`ParentA`。
因為`b.foo()`引用`super`,所以它被靜態地綁定到了`ChildB`/`ParentB`階層而不能被用于`ChildA`/`ParentA`階層。在ES6中沒有辦法解決這個限制。
如果你有一個不帶移花接木的靜態類階層,那么`super`的工作方式看起來很直觀。但公平地說,實施帶有`this`的編碼的一個主要好處正是這種靈活性。簡單地說,`class`?+?`super`要求你避免使用這樣的技術。
你能在對象設計上作出的選擇歸結為兩個:使用這些靜態的階層 ——?`class`,`extends`,和`super`將十分不錯 —— 要么放棄所有“山寨”類的企圖,而接受動態且靈活的,沒有類的對象和`[[Prototype]]`委托(參見本系列的?*this與對象原型*)。
#### 子類構造器
對類或子類來說構造器不是必需的;如果構造器被省略,這兩種情況下都會有一個默認構造器頂替上來。但是,對于一個直接的類和一個被擴展的類來說,頂替上來的默認構造器是不同的。
特別地,默認的子類構造器自動地調用父構造器,并且傳遞所有參數值。換句話說,你可以認為默認的子類構造器有些像這樣:
```source-js
constructor(...args) {
super(...args);
}
```
這是一個需要注意的重要細節。不是所有支持類的語言的子類構造器都會自動地調用父構造器。C++會,但Java不會。更重要的是,在前ES6的類中,這樣的自動“父構造器”調用不會發生。如果你曾經依賴于這樣的調用?*不會*?發生,按么當你將代碼轉換為ES6`class`時就要小心。
ES6子類構造器的另一個也許令人吃驚的偏差/限制是:在一個子類的構造器中,在`super(..)`被調用之前你不能訪問`this`。其中的原因十分微妙和復雜,但是可以歸結為是父構造器在實際上創建/初始化你的實例的`this`。前ES6中,它相反地工作;`this`對象被“子類構造器”創建,然后你使用這個“子類”的`this`上下文環境調用“父構造器”。
讓我們展示一下。這是前ES6版本:
```source-js
function Foo() {
this.a = 1;
}
function Bar() {
this.b = 2;
Foo.call( this );
}
// `Bar` “擴展” `Foo`
Bar.prototype = Object.create( Foo.prototype );
```
但是這個ES6等價物不允許:
```source-js
class Foo {
constructor() { this.a = 1; }
}
class Bar extends Foo {
constructor() {
this.b = 2; // 在`super()`之前不允許
super(); // 可以通過調換這兩個語句修正
}
}
```
在這種情況下,修改很簡單。只要在子類`Bar`的構造器中調換兩個語句的位置就行了。但是,如果你曾經依賴于前ES6可以跳過“父構造器”調用的話,就要小心這不再被允許了。
#### `extend`原生類型
新的`class`和`extend`設計中最值得被歡呼的好處之一,就是(終于!)能夠為內建原生類型,比如`Array`,創建子類。考慮如下代碼:
```source-js
class MyCoolArray extends Array {
first() { return this[0]; }
last() { return this[this.length - 1]; }
}
var a = new MyCoolArray( 1, 2, 3 );
a.length; // 3
a; // [1,2,3]
a.first(); // 1
a.last(); // 3
```
在ES6之前,可以使用手動的對象創建并將它鏈接到`Array.prototype`來制造一個`Array`的“子類”的山寨版,但它僅能部分地工作。它缺失了一個真正數組的特殊行為,比如自動地更新`length`屬性。ES6子類應該可以如我們盼望的那樣使用“繼承”與增強的行為來完整地工作!
另一個常見的前ES6“子類”的限制與`Error`對象有關,在創建自定義的錯誤“子類”時。當純粹的`Error`被創建時,它們自動地捕獲特殊的`stack`信息,包括錯誤被創建的行號和文件。前ES6的自定義錯誤“子類”沒有這樣的特殊行為,這嚴重地限制了它們的用處。
ES6前來拯救:
```source-js
class Oops extends Error {
constructor(reason) {
super(reason);
this.oops = reason;
}
}
// 稍后:
var ouch = new Oops( "I messed up!" );
throw ouch;
```
前面代碼段的`ouch`自定義錯誤對象將會向任何其他的純粹錯誤對象那樣動作,包括捕獲`stack`。這是一個巨大的改進!
### `new.target`
ES6引入了一個稱為?*元屬性*?的新概念(見第七章),用`new.target`的形式表示。
如果這看起來很奇怪,是的;將一個帶有`.`的關鍵字與一個屬性名配成一對,對JS來說絕對是不同尋常的模式。
`new.target`是一個在所有函數中可用的“魔法”值,雖然在普通的函數中它總是`undefined`。在任意的構造器中,`new.target`總是指向`new`實際直接調用的構造器,即便這個構造器是在一個父類中,而且是通過一個在子構造器中的`super(..)`調用被委托的。
```source-js
class Foo {
constructor() {
console.log( "Foo: ", new.target.name );
}
}
class Bar extends Foo {
constructor() {
super();
console.log( "Bar: ", new.target.name );
}
baz() {
console.log( "baz: ", new.target );
}
}
var a = new Foo();
// Foo: Foo
var b = new Bar();
// Foo: Bar <-- 遵照`new`的調用點
// Bar: Bar
b.baz();
// baz: undefined
```
`new.target`元屬性在類構造器中沒有太多作用,除了訪問一個靜態屬性/方法(見下一節)。
如果`new.target`是`undefined`,那么你就知道這個函數不是用`new`調用的。然后你就可以強制一個`new`調用,如果有必要的話。
### `static`
當一個子類`Bar`擴展一個父類`Foo`時,我們已經觀察到`Bar.prototype`被`[[Prototype]]`鏈接到`Foo.prototype`。但是額外地,`Bar()`被`[[Prototype]]`鏈接到`Foo()`。這部分可能就沒有那么明顯了。
但是,在你為一個類聲明`static`方法(不只是屬性)時它就十分有用,因為這些靜態方法被直接添加到這個類的函數對象上,不是函數對象的`prototype`對象上。考慮如下代碼:
```source-js
class Foo {
static cool() { console.log( "cool" ); }
wow() { console.log( "wow" ); }
}
class Bar extends Foo {
static awesome() {
super.cool();
console.log( "awesome" );
}
neat() {
super.wow();
console.log( "neat" );
}
}
Foo.cool(); // "cool"
Bar.cool(); // "cool"
Bar.awesome(); // "cool"
// "awesome"
var b = new Bar();
b.neat(); // "wow"
// "neat"
b.awesome; // undefined
b.cool; // undefined
```
小心不要被搞糊涂,認為`static`成員是在類的原型鏈上的。它們實際上存在與函數構造器中間的一個雙重/平行鏈條上。
#### `Symbol.species`構造器Getter
一個`static`可以十分有用的地方是為一個衍生(子)類設置`Symbol.species`getter(在語言規范內部稱為`@@species`)。這種能力允許一個子類通知一個父類應當使用什么樣的構造器 —— 當不打算使用子類的構造器本身時 —— 如果有任何父類方法需要產生新的實例的話。
舉個例子,在`Array`上的許多方法都創建并返回一個新的`Array`實例。如果你從`Array`定義一個衍生的類,但你想讓這些方法實際上繼續產生`Array`實例,而非從你的衍生類中產生實例,那么這就可以工作:
```source-js
class MyCoolArray extends Array {
// 強制`species`為父類構造器
static get [Symbol.species]() { return Array; }
}
var a = new MyCoolArray( 1, 2, 3 ),
b = a.map( function(v){ return v * 2; } );
b instanceof MyCoolArray; // false
b instanceof Array; // true
```
為了展示一個父類方法如何可以有些像`Array#map(..)`所做的那樣,使用一個子類型聲明,考慮如下代碼:
```source-js
class Foo {
// 將`species`推遲到衍生的構造器中
static get [Symbol.species]() { return this; }
spawn() {
return new this.constructor[Symbol.species]();
}
}
class Bar extends Foo {
// 強制`species`為父類構造器
static get [Symbol.species]() { return Foo; }
}
var a = new Foo();
var b = a.spawn();
b instanceof Foo; // true
var x = new Bar();
var y = x.spawn();
y instanceof Bar; // false
y instanceof Foo; // true
```
父類的`Symbol.species`使用`return this`來推遲到任意的衍生類,就像你通常期望的那樣。然后`Bar`手動地聲明`Foo`被用于這樣的實例創建。當然,一個衍生的類依然可以使用`new this.constructor(..)`生成它本身的實例。
## 復習
ES6引入了幾個在代碼組織上提供幫助的新特性:
* 迭代器提供了對數據和操作的序列化訪問。它們可以被`for..of`和`...`這樣的新語言特性消費。
* Generator是由一個迭代器控制的能夠在本地暫停/繼續的函數。它們可以被用于程序化地(并且是互動地,通過`yield`/`next(..)`消息傳遞)?*生成*?通過迭代器被消費的值。
* 模塊允許實現的細節的私有封裝帶有一個公開導出的API。模塊定義是基于文件的,單例的實例,并且在編譯時靜態地解析。
* 類為基于原型的編碼提供了更干凈的語法。`super`的到來也解決了在`[[Prototype]]`鏈中進行相對引用的刁鉆問題。
在你考慮通過采納ES6來改進你的JS項目體系結構時,這些新工具應當是你的第一站。