特別說明饰抒,為便于查閱详瑞,文章轉(zhuǎn)自https://github.com/getify/You-Dont-Know-JS
編寫JS代碼是一回事兒摘刑,而合理地組織它是另一回事兒伟葫。利用常見的組織和重用模式在很大程度上改善了你代碼的可讀性和可理解性裹芝。記撞看:代碼在與其他開發(fā)者交流上起的作用,與在給計算機(jī)喂指令上起的作用同樣重要嫂易。
ES6擁有幾種重要的特性可以顯著改善這些模式兄朋,包括:迭代器,generator怜械,模塊蜈漓,和類穆桂。
迭代器
迭代器(iterator) 是一種結(jié)構(gòu)化的模式,用于從一個信息源中以一次一個的方式抽取信息融虽。這種模式在程序設(shè)計中存在很久了享完。而且不可否認(rèn)的是,不知從什么時候起JS開發(fā)者們就已經(jīng)特別地設(shè)計并實現(xiàn)了迭代器有额,所以它根本不是什么新的話題般又。
ES6所做的是,為迭代器引入了一個隱含的標(biāo)準(zhǔn)化接口巍佑。許多在JavaScript中內(nèi)建的數(shù)據(jù)結(jié)構(gòu)現(xiàn)在都會暴露一個實現(xiàn)了這個標(biāo)準(zhǔn)的迭代器茴迁。而且你也可以構(gòu)建自己的遵循同樣標(biāo)準(zhǔn)的迭代器,來使互用性最大化萤衰。
迭代器是一種消費(fèi)數(shù)據(jù)的方法堕义,它是組織有順序的,相繼的脆栋,基于抽取的倦卖。
舉個例子,你可能實現(xiàn)一個工具椿争,它在每次被請求時產(chǎn)生一個新的唯一的標(biāo)識符怕膛。或者你可能循環(huán)一個固定的列表以輪流的方式產(chǎn)生一系列無限多的值秦踪『帜恚或者你可以在一個數(shù)據(jù)庫查詢的結(jié)果上添加一個迭代器來一次抽取一行結(jié)果。
雖然在JS中它們不經(jīng)常以這樣的方式被使用椅邓,但是迭代器還可以認(rèn)為是每次控制行為中的一個步驟柠逞。這會在考慮generator時得到相當(dāng)清楚的展示(參見本章稍后的“Generator”),雖然你當(dāng)然可以不使用generator而做同樣的事景馁。
接口
在本書寫作的時候板壮,ES6的25.1.1.2部分 (https://people.mozilla.org/~jorendorff/es6-draft.html#sec-iterator-interface) 詳述了Iterator
接口,它有如下的要求:
Iterator [必須]
next() {method}: 取得下一個IteratorResult
有兩個可選成員裁僧,有些迭代器用它們進(jìn)行了擴(kuò)展:
Iterator [可選]
return() {method}: 停止迭代并返回IteratorResult
throw() {method}: 通知錯誤并返回IteratorResult
接口IteratorResult
被規(guī)定為:
IteratorResult
value {property}: 當(dāng)前的迭代值或最終的返回值
(如果它的值為`undefined`个束,是可選的)
done {property}: 布爾值,指示完成的狀態(tài)
注意: 我稱這些接口是隱含的聊疲,不是因為它們沒有在語言規(guī)范中被明確地被說出來 —— 它們被說出來了茬底!—— 而是因為它們沒有作為可以直接訪問的對象暴露給代碼。在ES6中获洲,JavaScript不支持任何“接口”的概念阱表,所以在你自己的代碼中遵循它們純粹是慣例上的。但是,不論JS在何處需要一個迭代器 —— 例如在一個for..of
循環(huán)中 —— 你提供的東西必須遵循這些接口最爬,否則代碼就會失敗涉馁。
還有一個Iterable
接口,它描述了一定能夠產(chǎn)生迭代器的對象:
Iterable
@@iterator() {method}: 產(chǎn)生一個迭代器
如果你回憶一下第二章的“內(nèi)建Symbol”爱致,@@iterator
是一種特殊的內(nèi)建symbol烤送,表示可以為對象產(chǎn)生迭代器的方法。
IteratorResult
IteratorResult
接口規(guī)定從任何迭代器操作的返回值都是這樣形式的對象:
{ value: .. , done: true / false }
內(nèi)建迭代器將總是返回這種形式的值糠悯,當(dāng)然帮坚,更多的屬性也允許出現(xiàn)在這個返回值中,如果有必要的話互艾。
例如试和,一個自定義的迭代器可能會在結(jié)果對象中加入額外的元數(shù)據(jù)(比如,數(shù)據(jù)是從哪里來的纫普,取得它花了多久阅悍,緩存過期的時間長度,下次請求的恰當(dāng)頻率昨稼,等等)节视。
注意: 從技術(shù)上講,在值為undefined
的情況下悦昵,value
是可選的肴茄,它將會被認(rèn)為是不存在或者是沒有被設(shè)置晌畅。因為不管它是表示的就是這個值還是完全不存在棋凳,訪問res.value
都將會產(chǎn)生undefined
拍棕,所以這個屬性的存在/不存在更大程度上是一個實現(xiàn)或者優(yōu)化(或兩者)的細(xì)節(jié)绰播,而非一個功能上的問題谬泌。
next()
迭代
讓我們來看一個數(shù)組贱鼻,它是一個可迭代對象,可以生成一個迭代器來消費(fèi)它的值:
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
上被調(diào)用時,它都將生成一個全新的迭代器医寿。大多數(shù)的數(shù)據(jù)結(jié)構(gòu)都會這么做惠拭,包括所有內(nèi)建在JS中的數(shù)據(jù)結(jié)構(gòu)涵亏。
然而麸恍,像事件隊列這樣的結(jié)構(gòu)也許只能生成一個單獨的迭代器(單例模式)〔蠼茫或者某種結(jié)構(gòu)可能在同一時間內(nèi)只允許存在一個唯一的迭代器抹沪,要求當(dāng)前的迭代器必須完成,才能創(chuàng)建一個新的瓤球。
前一個代碼段中的it
迭代器不會再你得到值3
時報告done: true
融欧。你必須再次調(diào)用next()
,實質(zhì)上越過數(shù)組末尾的值卦羡,才能得到完成信號done: true
噪馏。在這一節(jié)稍后會清楚地講解這種設(shè)計方式的原因,但是它通常被認(rèn)為是一種最佳實踐绿饵。
基本類型的字符串值也默認(rèn)地是可迭代對象:
var greeting = "hello world";
var it = greeting[Symbol.iterator]();
it.next(); // { value: "h", done: false }
it.next(); // { value: "e", done: false }
..
注意: 從技術(shù)上講欠肾,這個基本類型值本身不是可迭代對象,但多虧了“封箱”拟赊,"hello world"
被強(qiáng)制轉(zhuǎn)換為它的String
對象包裝形式刺桃,它 才是一個可迭代對象。更多信息參見本系列的 類型與文法吸祟。
ES6還包括幾種新的數(shù)據(jù)結(jié)構(gòu)瑟慈,稱為集合(參見第五章)。這些集合不僅本身就是可迭代對象欢搜,而且它們還提供API方法來生成一個迭代器封豪,例如:
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(..)
方法能夠可選地接受一個或多個參數(shù)谴轮。大多數(shù)內(nèi)建的迭代器不會實施這種能力炒瘟,雖然一個generator的迭代器絕對會這么做(參見本章稍后的“Generator”)。
根據(jù)一般的慣例第步,包括所有的內(nèi)建迭代器疮装,在一個已經(jīng)被耗盡的迭代器上調(diào)用next(..)
不是一個錯誤,而是簡單地持續(xù)返回結(jié)果{ value: undefined, done: true }
粘都。
可選的return(..)
和throw(..)
在迭代器接口上的可選方法 —— return(..)
和throw(..)
—— 在大多數(shù)內(nèi)建的迭代器上都沒有被實現(xiàn)廓推。但是,它們在generator的上下文環(huán)境中絕對有某些含義翩隧,所以更具體的信息可以參看“Generator”樊展。
return(..)
被定義為向一個迭代器發(fā)送一個信號,告知它消費(fèi)者代碼已經(jīng)完成而且不會再從它那里抽取更多的值。這個信號可以用于通知生產(chǎn)者(應(yīng)答next(..)
調(diào)用的迭代器)去實施一些可能的清理作業(yè)专缠,比如釋放/關(guān)閉網(wǎng)絡(luò)雷酪,數(shù)據(jù)庫,或者文件引用資源涝婉。
如果一個迭代器擁有return(..)
哥力,而且發(fā)生了可以自動被解釋為非正常或者提前終止消費(fèi)迭代器的任何情況墩弯,return(..)
就將會被自動調(diào)用吩跋。你也可以手動調(diào)用return(..)
。
return(..)
將會像next(..)
一樣返回一個IteratorResult
對象渔工。一般來說锌钮,你向return(..)
發(fā)送的可選值將會在這個IteratorResult
中作為value
發(fā)送回來,雖然在一些微妙的情況下這可能不成立引矩。
throw(..)
被用于向一個迭代器發(fā)送一個異常/錯誤信號轧粟,與return(..)
隱含的完成信號相比,它可能會被迭代器用于不同的目的脓魏。它不一定像return(..)
一樣暗示著迭代器的完全停止兰吟。
例如,在generator迭代器中茂翔,throw(..)
實際上會將一個被拋出的異常注射到generator暫停的執(zhí)行環(huán)境中混蔼,這個異常可以用try..catch
捕獲珊燎。一個未捕獲的throw(..)
異常將會導(dǎo)致generator的迭代器異常中止惭嚣。
注意: 根據(jù)一般的慣例,在return(..)
或throw(..)
被調(diào)用之后悔政,一個迭代器就不應(yīng)該在產(chǎn)生任何結(jié)果了晚吞。
迭代器循環(huán)
正如我們在第二章的“for..of
”一節(jié)中講解的,ES6的for..of
循環(huán)可以直接消費(fèi)一個規(guī)范的可迭代對象谋国。
如果一個迭代器也是一個可迭代對象槽地,那么它就可以直接與for..of
循環(huán)一起使用。通過給予迭代器一個簡單地返回它自身的Symbol.iterator
方法芦瘾,你就可以使它成為一個可迭代對象:
var it = {
// 使迭代器`it`成為一個可迭代對象
[Symbol.iterator]() { return this; },
next() { .. },
..
};
it[Symbol.iterator]() === it; // true
現(xiàn)在我們就可以用一個for..of
循環(huán)來消費(fèi)迭代器it
了:
for (var v of it) {
console.log( v );
}
為了完全理解這樣的循環(huán)如何工作捌蚊,回憶下第二章中的for..of
循環(huán)的for
等價物:
for (var v, res; (res = it.next()) && !res.done; ) {
v = res.value;
console.log( v );
}
如果你仔細(xì)觀察,你會發(fā)現(xiàn)it.next()
是在每次迭代之前被調(diào)用的近弟,然后res.done
才被查詢缅糟。如果res.done
是true
,那么這個表達(dá)式將會求值為false
于是這次迭代不會發(fā)生祷愉。
回憶一下之前我們建議說窗宦,迭代器一般不應(yīng)與最終預(yù)期的值一起返回done: true
∩馄模現(xiàn)在你知道為什么了。
如果一個迭代器返回了{ done: true, value: 42 }
赴涵,for..of
循環(huán)將完全扔掉值42
沐扳。因此,假定你的迭代器可能會被for..of
循環(huán)或它的for
等價物這樣的模式消費(fèi)的話句占,你可能應(yīng)當(dāng)?shù)鹊侥阋呀?jīng)返回了所有相關(guān)的迭代值之后才返回done: true
來表示完成沪摄。
警告: 當(dāng)然,你可以有意地將你的迭代器設(shè)計為將某些相關(guān)的value
與done: true
同時返回纱烘。但除非你將此情況在文檔中記錄下來杨拐,否則不要這么做,因為這樣會隱含地強(qiáng)制你的迭代器消費(fèi)者使用一種擂啥,與我們剛才描述的for..of
或它的手動等價物不同的模式來進(jìn)行迭代哄陶。
自定義迭代器
除了標(biāo)準(zhǔn)的內(nèi)建迭代器,你還可以制造你自己的迭代器哺壶!所有使它們可以與ES6消費(fèi)設(shè)施(例如屋吨,for..of
循環(huán)和...
操作符)進(jìn)行互動的代價就是遵循恰當(dāng)?shù)慕涌凇?/p>
讓我們試著構(gòu)建一個迭代器,它能夠以斐波那契(Fibonacci)數(shù)列的形式產(chǎn)生無限多的數(shù)字序列:
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
循環(huán)將會永遠(yuǎn)運(yùn)行下去至扰,這回破壞你的程序,因此可能不是我們想要的资锰!
方法Fib[Symbol.iterator]()
在被調(diào)用時返回帶有next()
和return(..)
方法的迭代器對象敢课。它的狀態(tài)通過變量n1
和n2
維護(hù)在閉包中。
接下來讓我們考慮一個迭代器绷杜,它被設(shè)計為執(zhí)行一系列(也叫隊列)動作直秆,一次一個:
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
上的迭代器步過在數(shù)組屬性actions
中找到的函數(shù),并每次執(zhí)行它們中的一個鞭盟,并傳入你傳遞給next(..)
的任何參數(shù)值圾结,并在標(biāo)準(zhǔn)的IteratorResult
對象中向你返回任何它返回的東西。
這是我們?nèi)绾问褂眠@個tasks
隊列:
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 }
這種特別的用法證實了迭代器可以是一種具有組織功能的模式齿诉,不僅僅是數(shù)據(jù)筝野。這也聯(lián)系著我們在下一節(jié)關(guān)于generator將要看到的東西。
你甚至可以更有創(chuàng)意一些鹃两,在一塊數(shù)據(jù)上定義一個表示元操作的迭代器遗座。例如,我們可以為默認(rèn)從0開始遞增至(或遞減至俊扳,對于負(fù)數(shù)來說)指定數(shù)字的一組數(shù)字定義一個迭代器。
考慮如下代碼:
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;
// 正向迭代還是負(fù)向迭代猛遍?
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);
}
// 負(fù)向迭代
else {
i = Math.max(top,i + inc);
}
// 這次迭代之后就完了号坡?
if (i == top) done = true;
return { value: i, done: false };
}
else {
return { done: true };
}
}
};
}
}
);
}
現(xiàn)在,這種創(chuàng)意給了我們什么技巧梯醒?
for (var i of 3) {
console.log( i );
}
// 0 1 2 3
[...-3]; // [0,-1,-2,-3]
這是一些有趣的技巧宽堆,雖然其實際用途有些值得商榷。但是再一次茸习,有人可能想知道為什么ES6沒有提供如此微小但討喜的特性呢畜隶?
如果我連這樣的提醒都沒給過你,那就是我的疏忽:像我在前面的代碼段中做的那樣擴(kuò)展原生原型号胚,是一件你需要小心并了解潛在的危害后才應(yīng)該做的事情籽慢。
在這樣的情況下,你與其他代碼或者未來的JS特性發(fā)生沖突的可能性非常低猫胁。但是要小心微小的可能性箱亿。并在文檔中為后人詳細(xì)記錄下你在做什么。
注意: 如果你想知道更多細(xì)節(jié)弃秆,我在這篇文章(http://blog.getify.com/iterating-es6-numbers/) 中詳細(xì)論述了這種特別的技術(shù)届惋。而且這段評論(http://blog.getify.com/iterating-es6-numbers/comment-page-1/#comment-535294)甚至為制造一個字符串字符范圍提出了一個相似的技巧。
消費(fèi)迭代器
我們已經(jīng)看到了使用for..of
循環(huán)來一個元素一個元素地消費(fèi)一個迭代器菠赚。但是還有一些其他的ES6結(jié)構(gòu)可以消費(fèi)迭代器脑豹。
讓我們考慮一下附著這個數(shù)組上的迭代器(雖然任何我們選擇的迭代器都將擁有如下的行為):
var a = [1,2,3,4,5];
擴(kuò)散操作符...
將完全耗盡一個迭代器『獠椋考慮如下代碼:
function foo(x,y,z,w,p) {
console.log( x + y + z + w + p );
}
foo( ...a ); // 15
...
還可以在一個數(shù)組內(nèi)部擴(kuò)散一個迭代器:
var b = [ 0, ...a, 6 ];
b; // [0,1,2,3,4,5,6]
數(shù)組解構(gòu)(參見第二章的“解構(gòu)”)可以部分地或者完全地(如果與一個...
剩余/收集操作符一起使用)消費(fèi)一個迭代器:
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
所有的函數(shù)都會運(yùn)行至完成峡捡,對吧击碗?換句話說,一旦一個函數(shù)開始運(yùn)行们拙,在它完成之前沒有任何東西能夠打斷它稍途。
至少對于到目前為止的JavaScript的整個歷史來說是這樣的。在ES6中砚婆,引入了一個有些異乎尋常的新形式的函數(shù)械拍,稱為generator。一個generator可以在運(yùn)行期間暫停它自己装盯,還可以立即或者稍后繼續(xù)運(yùn)行坷虑。所以顯然它沒有普通函數(shù)那樣的運(yùn)行至完成的保證。
另外埂奈,在運(yùn)行期間的每次暫停/繼續(xù)輪回都是一個雙向消息傳遞的好機(jī)會迄损,generator可以在這里返回一個值,而使它繼續(xù)的控制端代碼可以發(fā)回一個值。
就像前一節(jié)中的迭代器一樣淮野,有種方式可以考慮generator是什么,或者說它對什么最有用刚陡。對此沒有一個正確的答案氏捞,但我們將試著從幾個角度考慮碧聪。
注意: 關(guān)于generator的更多信息參見本系列的 異步與性能,還可以參見本書的第四章液茎。
語法
generator函數(shù)使用這種新語法聲明:
function *foo() {
// ..
}
*
的位置在功能上無關(guān)緊要逞姿。同樣的聲明還可以寫做以下的任意一種:
function *foo() { .. }
function* foo() { .. }
function * foo() { .. }
function*foo() { .. }
..
這里 唯一 的區(qū)別就是風(fēng)格的偏好。大多數(shù)其他的文獻(xiàn)似乎喜歡function* foo(..) { .. }
捆等。我喜歡function *foo(..) { .. }
滞造,所以這就是我將在本書剩余部分中表示它們的方法。
我這樣做的理由實質(zhì)上純粹是為了教學(xué)楚里。在這本書中断部,當(dāng)我引用一個generator函數(shù)時,我將使用*foo(..)
班缎,與普通函數(shù)的foo(..)
相對蝴光。我發(fā)現(xiàn)*foo(..)
與function *foo(..) { .. }
中*
的位置更加吻合。
另外达址,就像我們在第二章的簡約方法中看到的蔑祟,在對象字面量中有一種簡約generator形式:
var a = {
*foo() { .. }
};
我要說在簡約generator中,*foo() { .. }
要比* foo() { .. }
更自然沉唠。這進(jìn)一步表明了為何使用*foo()
匹配一致性疆虚。
一致性使理解與學(xué)習(xí)更輕松。
執(zhí)行一個Generator
雖然一個generator使用*
進(jìn)行聲明满葛,但是你依然可以像一個普通函數(shù)那樣執(zhí)行它:
foo();
你依然可以傳給它參數(shù)值径簿,就像:
function *foo(x,y) {
// ..
}
foo( 5, 10 );
主要區(qū)別在于,執(zhí)行一個generator嘀韧,比如foo(5,10)
篇亭,并不實際運(yùn)行g(shù)enerator中的代碼。取而代之的是锄贷,它生成一個迭代器來控制generator執(zhí)行它的代碼译蒂。
我們將在稍后的“迭代器控制”中回到這個話題,但是簡要地說:
function *foo() {
// ..
}
var it = foo();
// 要開始/推進(jìn)`*foo()`谊却,調(diào)用
// `it.next(..)`
yield
Generator還有一個你可以在它們內(nèi)部使用的新關(guān)鍵字柔昼,用來表示暫停點:yield
⊙妆妫考慮如下代碼:
function *foo() {
var x = 10;
var y = 20;
yield;
var z = x + y;
}
在這個*foo()
generator中捕透,前兩行的操作將會在開始時運(yùn)行,然后yield
將會暫停這個generator。如果這個generator被繼續(xù)激率,*foo()
的最后一行將運(yùn)行咳燕。在一個generator中yield
可以出現(xiàn)任意多次(或者勿决,在技術(shù)上講乒躺,根本不出現(xiàn)!)低缩。
你甚至可以在一個循環(huán)內(nèi)部放置yield
嘉冒,它可以表示一個重復(fù)的暫停點。事實上咆繁,一個永不完成的循環(huán)就意味著一個永不完成的generator讳推,這是完全合法的,而且有時候完全是你需要的玩般。
yield
不只是一個暫停點银觅。它是在暫停generator時發(fā)送出一個值的表達(dá)式。這里是一個位于generator中的while..true
循環(huán)坏为,它每次迭代時yield
出一個新的隨機(jī)數(shù):
function *foo() {
while (true) {
yield Math.random();
}
}
yield ..
表達(dá)式不僅發(fā)送一個值 —— 不帶值的yield
與yield undefined
相同 —— 它還接收(也就是究驴,被替換為)最終的繼續(xù)值≡确考慮如下代碼:
function *foo() {
var x = yield 10;
console.log( x );
}
這個generator在暫停它自己時將首先yield
出值10
洒忧。當(dāng)你繼續(xù)這個generator時 —— 使用我們先前提到的it.next(..)
—— 無論你使用什么值繼續(xù)它,這個值都將替換/完成整個表達(dá)式yield 10
够颠,這意味著這個值將被賦值給變量x
一個yield..
表達(dá)式可以出現(xiàn)在任意普通表達(dá)式可能出現(xiàn)的地方熙侍。例如:
function *foo() {
var arr = [ yield 1, yield 2, yield 3 ];
console.log( arr, yield 4 );
}
這里的*foo()
有四個yield ..
表達(dá)式。其中每個yield
都會導(dǎo)致generator暫停以等待一個繼續(xù)值履磨,這個繼續(xù)值稍后被用于各個表達(dá)式環(huán)境中蛉抓。
yield
在技術(shù)上講不是一個操作符,雖然像yield 1
這樣使用時看起來確實很像剃诅。因為yield
可以像var x = yield
這樣完全通過自己被使用巷送,所以將它認(rèn)為是一個操作符有時令人困惑。
從技術(shù)上講综苔,yield ..
與a = 3
這樣的賦值表達(dá)式擁有相同的“表達(dá)式優(yōu)先級” —— 概念上和操作符優(yōu)先級很相似惩系。這意味著yield ..
基本上可以出現(xiàn)在任何a = 3
可以合法出現(xiàn)的地方。
讓我們展示一下這種對稱性:
var a, b;
a = 3; // 合法
b = 2 + a = 3; // 不合法
b = 2 + (a = 3); // 合法
yield 3; // 合法
a = 2 + yield 3; // 不合法
a = 2 + (yield 3); // 合法
注意: 如果你好好考慮一下如筛,認(rèn)為一個yield ..
表達(dá)式與一個賦值表達(dá)式的行為相似在概念上有些道理堡牡。當(dāng)一個被暫停的generator被繼續(xù)時,它就以一種與被這個繼續(xù)值“賦值”區(qū)別不大的方式杨刨,被這個值完成/替換晤柄。
要點:如果你需要yield ..
出現(xiàn)在a = 3
這樣的賦值本不被允許出現(xiàn)的位置,那么它就需要被包在一個( )
中妖胀。
因為yield
關(guān)鍵字的優(yōu)先級很低芥颈,幾乎任何出現(xiàn)在yield ..
之后的表達(dá)式都會在被yield
發(fā)送之前首先被計算惠勒。只有擴(kuò)散操作符...
和逗號操作符,
擁有更低的優(yōu)先級,這意味著他們會在yield
已經(jīng)被求值之后才會被處理爬坑。
所以正如帶有多個操作符的普通語句一樣纠屋,存在另一個可能需要( )
來覆蓋(提升)yield
的低優(yōu)先級的情況,就像這些表達(dá)式之間的區(qū)別:
yield 2 + 3; // 與`yield (2 + 3)`相同
(yield 2) + 3; // 首先`yield 2`盾计,然后`+ 3`
和=
賦值一樣售担,yield
也是“右結(jié)合性”的,這意味著多個接連出現(xiàn)的yield
表達(dá)式被視為從右到左被( .. )
分組署辉。所以族铆,yield yield yield 3
將被視為yield (yield (yield 3))
。像((yield) yield) yield 3
這樣的“左結(jié)合性”解釋沒有意義哭尝。
和其他操作符一樣哥攘,yield
與其他操作符或yield
組合時為了使你的意圖沒有歧義,使用( .. )
分組是一個好主意材鹦,即使這不是嚴(yán)格要求的逝淹。
注意: 更多關(guān)于操作符優(yōu)先級和結(jié)合性的信息,參見本系列的 類型與文法侠姑。
yield *
與*
使一個function
聲明成為一個function *
generator聲明的方式一樣创橄,一個*
使yield
成為一個機(jī)制非常不同的yield *
,稱為 yield委托莽红。從文法上講妥畏,yield *..
的行為與yield ..
相同,就像在前一節(jié)討論過的那樣安吁。
yield * ..
需要一個可迭代對象醉蚁;然后它調(diào)用這個可迭代對象的迭代器,并將它自己的宿主generator的控制權(quán)委托給那個迭代器鬼店,直到它被耗盡网棍。考慮如下代碼:
function *foo() {
yield *[1,2,3];
}
注意: 與generator聲明中*
的位置(早先討論過)一樣妇智,在yield *
表達(dá)式中的*
的位置在風(fēng)格上由你來決定滥玷。大多數(shù)其他文獻(xiàn)偏好yield* ..
,但是我喜歡yield *..
巍棱,理由和我們已經(jīng)討論過的相同惑畴。
值[1,2,3]
產(chǎn)生一個將會步過它的值的迭代器,所以generator*foo()
將會在被消費(fèi)時產(chǎn)生這些值航徙。另一種說明這種行為的方式是如贷,yield委托到了另一個generator:
function *foo() {
yield 1;
yield 2;
yield 3;
}
function *bar() {
yield *foo();
}
當(dāng)*bar()
調(diào)用*foo()
產(chǎn)生的迭代器通過yield *
受到委托,意味著無論*foo()
產(chǎn)生什么值都會被*bar()
產(chǎn)生。
在yield ..
中表達(dá)式的完成值來自于使用it.next(..)
繼續(xù)generator杠袱,而yield *..
表達(dá)式的完成值來自于受到委托的迭代器的返回值(如果有的話)尚猿。
內(nèi)建的迭代器一般沒有返回值,正如我們在本章早先的“迭代器循環(huán)”一節(jié)的末尾講過的楣富。但是如果你定義你自己的迭代器(或者generator)凿掂,你就可以將它設(shè)計為return
一個值,yield *..
將會捕獲它:
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
是表達(dá)式yield *foo()
的完成值,然后它被賦值給x
秉馏。
因為yield *
可以調(diào)用另一個generator(通過委托到它的迭代器的方式)耙旦,它還可以通過調(diào)用自己來實施某種generator遞歸:
function *foo(x) {
if (x < 3) {
x = yield *foo( x + 1 );
}
return x * 2;
}
foo( 1 );
取得foo(1)
的結(jié)果并調(diào)用迭代器的next()
來使它運(yùn)行它的遞歸步驟,結(jié)果將是24
萝究。第一次*foo()
運(yùn)行時x
擁有值1
免都,它是x < 3
。x + 1
被遞歸地傳遞到*foo(..)
帆竹,所以之后的x
是2
绕娘。再一次遞歸調(diào)用導(dǎo)致x
為3
。
現(xiàn)在栽连,因為x < 3
失敗了险领,遞歸停止,而且return 3 * 2
將6
給回前一個調(diào)用的yeild *..
表達(dá)式秒紧,它被賦值給x
绢陌。另一個return 6 * 2
返回12
給前一個調(diào)用的x
。最終12 * 2
熔恢,即24
脐湾,從generator*foo(..)
運(yùn)行的完成中被返回。
迭代器控制
早先叙淌,我們簡要地介紹了generator是由迭代器控制的概念〕诱疲現(xiàn)在讓我們完整地深入這個話題。
回憶一下前一節(jié)的遞歸*for(..)
鹰霍。這是我們?nèi)绾芜\(yùn)行它:
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 ..
表達(dá)式衅谷。而yield *
只是通過遞歸調(diào)用保持當(dāng)前的迭代步驟繼續(xù)運(yùn)行下去椒拗。所以,僅僅對迭代器的next()
函數(shù)進(jìn)行一次調(diào)用就完全地運(yùn)行了generator。
現(xiàn)在讓我們考慮一個有多個步驟并且因此有多個產(chǎn)生值的generator:
function *foo() {
yield 1;
yield 2;
yield 3;
}
我們已經(jīng)知道我們可以是使用一個for..of
循環(huán)來消費(fèi)一個迭代器蚀苛,即便它是一個附著在*foo()
這樣的generator上:
for (var v of foo()) {
console.log( v );
}
// 1 2 3
注意: for..of
循環(huán)需要一個可迭代對象在验。一個generator函數(shù)引用(比如foo
)本身不是一個可迭代對象;你必須使用foo()
來執(zhí)行它以得到迭代器(它也是一個可迭代對象堵未,正如我們在本章早先講解過的)腋舌。理論上你可以使用一個實質(zhì)上僅僅執(zhí)行return this()
的Symbol.iterator
函數(shù)來擴(kuò)展GeneratorPrototype
(所有g(shù)enerator函數(shù)的原型)。這將使foo
引用本身成為一個可迭代對象渗蟹,也就意味著for (var v of foo) { .. }
(注意在foo
上沒有()
)將可以工作块饺。
讓我們手動迭代這個generator:
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 }
如果你仔細(xì)觀察,這里有三個yield
語句和四個next()
調(diào)用雌芽。這可能看起來像是一個奇怪的不匹配授艰。事實上,假定所有的東西都被求值并且generator完全運(yùn)行至完成的話世落,next()
調(diào)用將總是比yield
表達(dá)式多一個淮腾。
但是如果你相反的角度觀察(從里向外而不是從外向里),yield
和next()
之間的匹配就顯得更有道理屉佳。
回憶一下谷朝,yield ..
表達(dá)式將被你用于繼續(xù)generator的值完成。這意味著你傳遞給next(..)
的參數(shù)值將完成任何當(dāng)前暫停中等待完成的yield ..
表達(dá)式武花。
讓我們這樣展示一下這種視角:
function *foo() {
var x = yield 1;
var y = yield 2;
var z = yield 3;
console.log( x, y, z );
}
在這個代碼段中圆凰,每個yield ..
都送出一個值(1
,2
体箕,3
)专钉,但更直接的是,它暫停了generator來等待一個值干旁。換句話說驶沼,它就像在問這樣一個問題,“我應(yīng)當(dāng)在這里用什么值争群?我會在這里等你告訴我回怜。”
現(xiàn)在换薄,這是我們?nèi)绾慰刂?code>*foo()來啟動它:
var it = foo();
it.next(); // { value: 1, done: false }
這第一個next()
調(diào)用從generator初始的暫停狀態(tài)啟動了它玉雾,并運(yùn)行至第一個yield
。在你調(diào)用第一個next()
的那一刻轻要,并沒有yield ..
表達(dá)式等待完成复旬。如果你給第一個next()
調(diào)用傳遞一個值,目前它會被扔掉冲泥,因為沒有yield
等著接受這樣的一個值驹碍。
注意: 一個“ES6之后”時間表中的早期提案 將 允許你在generator內(nèi)部通過一個分離的元屬性(見第七章)來訪問一個被傳入初始next(..)
調(diào)用的值壁涎。
現(xiàn)在,讓我們回答那個未解的問題志秃,“我應(yīng)當(dāng)給x
賦什么值怔球?” 我們將通過給 下一個 next(..)
調(diào)用發(fā)送一個值來回答:
it.next( "foo" ); // { value: 2, done: false }
現(xiàn)在,x
將擁有值"foo"
浮还,但我們也問了一個新的問題竟坛,“我應(yīng)當(dāng)給y
賦什么值?”
it.next( "bar" ); // { value: 3, done: false }
答案給出了钧舌,另一個問題被提出了担汤。最終答案:
it.next( "baz" ); // "foo" "bar" "baz"
// { value: undefined, done: true }
現(xiàn)在,每一個yield ..
的“問題”是如何被 下一個 next(..)
調(diào)用回答的洼冻,所以我們觀察到的那個“額外的”next()
調(diào)用總是使一切開始的那一個崭歧。
讓我們把這些步驟放在一起:
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 }
在生成器的每次迭代都簡單地為消費(fèi)者生成一個值的情況下,你可認(rèn)為一個generator是一個值的生成器碘赖。
但是在更一般的意義上驾荣,也許將generator認(rèn)為是一個受控制的,累進(jìn)的代碼執(zhí)行過程更恰當(dāng)普泡,與早先“自定義迭代器”一節(jié)中的tasks
隊列的例子非常相像。
注意: 這種視角正是我們將如何在第四章中重溫generator的動力审编。特別是撼班,next(..)
沒有理由一定要在前一個next(..)
完成之后立即被調(diào)用。雖然generator的內(nèi)部執(zhí)行環(huán)境被暫停了垒酬,程序的其他部分仍然沒有被阻塞砰嘁,這包括控制generator什么時候被繼續(xù)的異步動作能力。
提前完成
正如我們在本章早先講過的勘究,連接到一個generator的迭代器支持可選的return(..)
和throw(..)
方法矮湘。它們倆都有立即中止一個暫停的的generator的效果。
考慮如下代碼:
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)
有點像強(qiáng)制一個return x
就在那個時刻被處理口糕,這樣你就立即得到這個指定的值缅阳。一旦一個generator完成,無論是正常地還是像展示的那樣提前地景描,它就不再處理任何代碼或返回任何值了十办。
return(..)
除了可以手動調(diào)用,它還在迭代的最后被任何ES6中消費(fèi)迭代器的結(jié)構(gòu)自動調(diào)用超棺,比如for..of
循環(huán)和...
擴(kuò)散操作符向族。
這種能力的目的是,在控制端的代碼不再繼續(xù)迭代generator時它可以收到通知棠绘,這樣它就可能做一些清理工作(釋放資源件相,復(fù)位狀態(tài)再扭,等等)。與普通函數(shù)的清理模式完全相同夜矗,達(dá)成這個目的的主要方法是使用一個finally
子句:
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
子句內(nèi)部霍衫!它是有效和合法的,但這確實是一個可怕的主意侯养。它在某種意義上推遲了return(..)
調(diào)用的完成敦跌,因為在finally
子句中的任何yield ..
表達(dá)式都被遵循來暫停和發(fā)送消息;你不會像期望的那樣立即得到一個完成的generator逛揩∧基本上沒有任何好的理由去選擇這種瘋狂的 壞的部分,所以避免這么做辩稽!
前一個代碼段除了展示return(..)
如何在中止generator的同時觸發(fā)finally
子句惧笛,它還展示了一個generator在每次被調(diào)用時都產(chǎn)生一個全新的迭代器。事實上逞泄,你可以并發(fā)地使用連接到相同generator的多個迭代器:
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 }
提前中止
你可以調(diào)用throw(..)
來代替return(..)
調(diào)用患整。就像return(x)
實質(zhì)上在generator當(dāng)前的暫停點上注入了一個return x
一樣,調(diào)用throw(x)
實質(zhì)上就像在暫停點上注入了一個throw x
喷众。
除了處理異常的行為(我們在下一節(jié)講解這對try
子句意味著什么)各谚,throw(..)
產(chǎn)生相同的提前完成 —— 在generator當(dāng)前的暫停點中止它的運(yùn)行。例如:
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
這一行到千,而且沒有東西處理這個異常昌渤,它立即傳播回外面的調(diào)用端代碼,調(diào)用端代碼使用了一個try..catch
來處理了它憔四。
與return(..)
不同的是膀息,迭代器的throw(..)
方法絕不會被自動調(diào)用。
當(dāng)然了赵,雖然沒有在前面的代碼段中展示潜支,但如果當(dāng)你調(diào)用throw(..)
時有一個try..finally
子句等在generator內(nèi)部的話,這個finally
子句將會在異常被傳播回調(diào)用端代碼之前有機(jī)會運(yùn)行柿汛。
錯誤處理
正如我們已經(jīng)得到的提示冗酿,generator中的錯誤處理可以使用try..catch
表達(dá),它在上行和下行兩個方向都可以工作苛茂。
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 *
委托在兩個方向上傳播:
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 }
當(dāng)*foo()
調(diào)用yield 1
時已烤,值1
原封不動地穿過了*bar()
,就像我們已經(jīng)看到過的那樣妓羊。
但這個代碼段最有趣的部分是胯究,當(dāng)*foo()
調(diào)用throw "foo: e2"
時,這個錯誤傳播到了*bar()
并立即被*bar()
的try..catch
塊兒捕獲躁绸。錯誤沒有像值1
那樣穿過*bar()
裕循。
然后*bar()
的catch
將err
普通地輸出("foo: e2"
)之后*bar()
就正常結(jié)束了臣嚣,這就是為什么迭代器結(jié)果{ value: undefined, done: true }
從it.next()
中返回。
如果*bar()
沒有用try..catch
環(huán)繞著yield *..
表達(dá)式剥哑,那么錯誤將理所當(dāng)然地一直傳播出來硅则,而且在它傳播的路徑上依然會完成(中止)*bar()
。
轉(zhuǎn)譯一個Generator
有可能在ES6之前的環(huán)境中表達(dá)generator的能力嗎株婴?事實上是可以的怎虫,而且有好幾種了不起的工具在這么做,包括最著名的Facebook的Regenerator工具 (https://facebook.github.io/regenerator/)困介。
但為了更好地理解generator大审,讓我們試著手動轉(zhuǎn)換一下∽ǎ基本上講徒扶,我們將制造一個簡單的基于閉包的狀態(tài)機(jī)。
我們將使原本的generator非常簡單:
function *foo() {
var x = yield 42;
console.log( x );
}
開始之前根穷,我們將需要一個我們能夠執(zhí)行的稱為foo()
的函數(shù)姜骡,它需要返回一個迭代器:
function foo() {
// ..
return {
next: function(v) {
// ..
}
// 我們將省略`return(..)`和`throw(..)`
};
}
現(xiàn)在,我們需要一些內(nèi)部變量來持續(xù)跟蹤我們的“generator”的邏輯走到了哪一個步驟屿良。我們稱它為state
圈澈。我們將有三種狀態(tài):起始狀態(tài)的0
,等待完成yield
表達(dá)式的1
管引,和generator完成的2
士败。
每次next(..)
被調(diào)用時,我們需要處理下一個步驟褥伴,然后遞增state
。為了方便漾狼,我們將每個步驟放在一個switch
語句的case
子句中重慢,并且我們將它放在一個next(..)
可以調(diào)用的稱為nextState(..)
的內(nèi)部函數(shù)中。另外逊躁,因為x
是一個橫跨整個“generator”作用域的變量似踱,所以它需要存活在nextState(..)
函數(shù)的外部。
這是將它們放在一起(很明顯稽煤,為了使概念的展示更清晰核芽,它經(jīng)過了某些簡化):
function foo() {
function nextState(v) {
switch (state) {
case 0:
state++;
// `yield`表達(dá)式
return 42;
case 1:
state++;
// `yield`表達(dá)式完成了
x = v;
console.log( x );
// 隱含的`return`
return undefined;
// 無需處理狀態(tài)`2`
}
}
var state = 0, x;
return {
next: function(v) {
var ret = nextState( v );
return { value: ret, done: (state == 2) };
}
// 我們將省略`return(..)`和`throw(..)`
};
}
最后,讓我們測試一下我們的前ES6“generator”:
var it = foo();
it.next(); // { value: 42, done: false }
it.next( 10 ); // 10
// { value: undefined, done: true }
不賴吧酵熙?希望這個練習(xí)能在你的腦中鞏固這個概念:generator實際上只是狀態(tài)機(jī)邏輯的簡單語法轧简。這使它們可以廣泛地應(yīng)用。
Generator的使用
我們現(xiàn)在非常深入地理解了generator如何工作匾二,那么哮独,它們在什么地方有用拳芙?
我們已經(jīng)看過了兩種主要模式:
-
生產(chǎn)一系列值: 這種用法可以很簡單(例如,隨機(jī)字符串或者遞增的數(shù)字)皮璧,或者它也可以表達(dá)更加結(jié)構(gòu)化的數(shù)據(jù)訪問(例如舟扎,迭代一個數(shù)據(jù)庫查詢結(jié)果的所有行)。
這兩種方式中悴务,我們使用迭代器來控制generator睹限,這樣就可以為每次
next(..)
調(diào)用執(zhí)行一些邏輯。在數(shù)據(jù)解構(gòu)上的普通迭代器只不過生成值而沒有任何控制邏輯讯檐。 -
串行執(zhí)行的任務(wù)隊列: 這種用法經(jīng)常用來表達(dá)一個算法中步驟的流程控制羡疗,其中每一步都要求從某些外部數(shù)據(jù)源取得數(shù)據(jù)。對每塊兒數(shù)據(jù)的請求可能會立即滿足裂垦,或者可能會異步延遲地滿足顺囊。
從generator內(nèi)部代碼的角度來看,在
yield
的地方蕉拢,同步或異步的細(xì)節(jié)是完全不透明的特碳。另外,這些細(xì)節(jié)被有意地抽象出去晕换,如此就不會讓這樣的實現(xiàn)細(xì)節(jié)把各個步驟間自然的午乓,順序的表達(dá)搞得模糊不清。抽象還意味著實現(xiàn)可以被替換/重構(gòu)闸准,而根本不用碰generator中的代碼益愈。
當(dāng)根據(jù)這些用法觀察generator時,它們的含義要比僅僅是手動狀態(tài)機(jī)的一種不同或更好的語法多多了夷家。它們是一種用于組織和控制有序地生產(chǎn)與消費(fèi)數(shù)據(jù)的強(qiáng)大工具蒸其。
模塊
我覺得這樣說并不夸張:在所有的JavaScript代碼組織模式中最重要的就是,而且一直是库快,模塊摸袁。對于我自己來說,而且我認(rèn)為對廣大典型的技術(shù)社區(qū)來說义屏,模塊模式驅(qū)動著絕大多數(shù)代碼靠汁。
過去的方式
傳統(tǒng)的模塊模式基于一個外部函數(shù),它帶有內(nèi)部變量和函數(shù)闽铐,以及一個被返回的“公有API”蝶怔。這個“公有API”帶有對內(nèi)部變量和功能擁有閉包的方法。它經(jīng)常這樣表達(dá):
function Hello(name) {
function greeting() {
console.log( "Hello " + name + "!" );
}
// 公有API
return {
greeting: greeting
};
}
var me = Hello( "Kyle" );
me.greeting(); // Hello Kyle!
這個Hello(..)
模塊通過被后續(xù)調(diào)用可以產(chǎn)生多個實例兄墅。有時踢星,一個模塊為了作為一個單例(也就是,只需要一個實例)而只被調(diào)用一次察迟,這樣的情況下常見的是一種前面代碼段的變種耳高,使用IIFE:
var me = (function Hello(name){
function greeting() {
console.log( "Hello " + name + "!" );
}
// 公有API
return {
greeting: greeting
};
})( "Kyle" );
me.greeting(); // Hello Kyle!
這種模式是經(jīng)受過檢驗的。它也足夠靈活所踊,以至于在許多不同的場景下可以有大量的各種變化。
其中一種最常見的是異步模塊定義(AMD)继薛,另一種是統(tǒng)一模塊定義(UMD)慈鸠。我們不會在這里涵蓋這些特定的模式和技術(shù)青团,但是它們在網(wǎng)上的許多地方有大量的講解。
向前邁進(jìn)
在ES6中咖楣,我們不再需要依賴外圍函數(shù)和閉包來為我們提供模塊支持了督笆。ES6模塊擁有頭等語法上和功能上的支持。
在我們接觸這些具體語法之前诱贿,重要的是要理解ES6模塊與你以前曾經(jīng)用過的模塊比較起來娃肿,在概念上的一些相當(dāng)顯著的不同之處:
-
ES6使用基于文件的模塊,這意味著一個模塊一個文件珠十。目前料扰,沒有標(biāo)準(zhǔn)的方法將多個模塊組合到一個文件中。
這意味著如果你要直接把ES6模塊加載到一個瀏覽器web應(yīng)用中的話焙蹭,你將個別地加載它們记罚,不是像常見的那樣為了性能優(yōu)化而作為一個單獨文件中的一個巨大的包加載。
預(yù)計同時期到來的HTTP/2將會大幅緩和這種性能上的顧慮壳嚎,因為它工作在一個持續(xù)的套接字連接上,因而可以用并行的末早,互相交錯的方式非常高效地加載許多小文件烟馅。
-
一個ES6模塊的API是靜態(tài)的。這就是說然磷,你在模塊的公有API上靜態(tài)地定義所有被導(dǎo)出的頂層內(nèi)容郑趁,而這些內(nèi)容導(dǎo)出之后不能被修改。
有些用法習(xí)慣于能夠提供動態(tài)API定義姿搜,它的方法可以根據(jù)運(yùn)行時的條件被增加/刪除/替換寡润。這些用法要么必須改變以適應(yīng)ES6靜態(tài)API捆憎,要么它們就不得不將屬性/方法的動態(tài)修改限制在一個內(nèi)層對象中。
ES6模塊都是單例梭纹。也就是躲惰,模塊只有一個維持它狀態(tài)的實例。每次你將這個模塊導(dǎo)入到另一個模塊時变抽,你得到的都是一個指向中央實例的引用础拨。如果你想要能夠產(chǎn)生多個模塊實例,你的模塊將需要提供某種工廠來這么做绍载。
-
你在模塊的公有API上暴露的屬性和方法不是值和引用的普通賦值诡宗。它們是在你內(nèi)部模塊定義中的標(biāo)識符的實際綁定(幾乎就是指針)。
在前ES6的模塊中击儡,如果你將一個持有像數(shù)字或者字符串這樣基本類型的屬性放在你的共有API中塔沃,那么這個屬性是通過值拷貝賦值的,任何對相應(yīng)內(nèi)部變量的更新都將是分離的阳谍,不會影響在API對象上的共有拷貝蛀柴。
在ES6中,導(dǎo)出一個本地私有變量边坤,即便它當(dāng)前持有一個基本類型的字符串/數(shù)字/等等名扛,導(dǎo)出的都是這個變量的一個綁定。如果這個模塊改變了這個變量的值茧痒,外部導(dǎo)入的綁定就會解析為那個新的值肮韧。
-
導(dǎo)入一個模塊和靜態(tài)地請求它被加載是同一件事情(如果它還沒被加載的話)。如果你在瀏覽器中旺订,這意味著通過網(wǎng)絡(luò)的阻塞加載弄企。如果你在服務(wù)器中,它是一個通過文件系統(tǒng)的阻塞加載区拳。
但是拘领,不要對它在性能的影響上驚慌。因為ES6模塊是靜態(tài)定義的樱调,導(dǎo)入的請求可以被靜態(tài)地掃描变汪,并提前加載,甚至是在你使用這個模塊之前锌杀。
ES6并沒有實際規(guī)定或操縱這些加載請求如何工作的機(jī)制时甚。有一個模塊加載器的分離概念,它讓每一個宿主環(huán)境(瀏覽器乞而,Node.js送悔,等等)為該環(huán)境提供合適的默認(rèn)加載器。一個模塊的導(dǎo)入使用一個字符串值來表示從哪里去取得模塊(URL,文件路徑欠啤,等等)荚藻,但是這個值在你的程序中是不透明的,它僅對加載器自身有意義洁段。
如果你想要比默認(rèn)加載器提供的更細(xì)致的控制能力应狱,你可以定義你自己的加載器 —— 默認(rèn)加載器基本上不提供任何控制,它對于你的程序代碼是完全隱藏的眉撵。
如你所見侦香,ES6模塊將通過封裝,控制共有API纽疟,以及應(yīng)用依賴導(dǎo)入來服務(wù)于所有的代碼組織需求罐韩。但是它們用一種非常特別的方式來這樣做,這可能與你已經(jīng)使用多年的模塊方式十分接近污朽,也肯能差得很遠(yuǎn)散吵。