提示
: 本文是 github 上《Understanding ECMAScript 6》 的筆記整理,程式碼示例也來源於此。大家有時間可以直接讀這本書。雖是英文,但通俗易懂,非常推薦。
之前面試時有被問到為什麼會有 Generator, 還好沒懵逼。想知道的嗎,往下翻。
這裡主要介紹 generator
的由來和一些基本概念,已經瞭解了或者想了解 generator
高階用法的可以看這篇 怎麼往 Generator 裡拋個錯?
迴圈的問題
ES5
裡遍歷一個陣列需要這樣:
const colors = ["red", "green", "blue"];
for (var i = 0, len = colors.length; i < len; i++) {
console.log(colors[i]);
}
複製程式碼
可以看出:
- 它既要追蹤下標位置,
- 還要判斷迴圈何時停止。
這段程式碼邏輯簡單, 但寫法複雜而枯燥。而且很常用,所以很容易因手抖而出bug。為了簡化寫法,降低出錯機率,ES6
引入了一些新的語法,其中一個是 iterator
。
什麼是 iterator
iterator
也是一種物件,不過它有著專為迭代而設計的介面。它有next
方法,該方法返回一個包含 value
和 done
兩個屬性的物件 (下稱 result
)。前者是迭代的值,後者是表明迭代是否完成的標誌 -- 布林值: true
表示迭代完成,false
表示沒有。iterator
內部有指向迭代位置的指標,每次呼叫next
, 自動移動指標並返回相應的 result
。
下面是一個自定義的 iterator
:
function createIterator(items) {
var i = 0;
return {
next: function () {
var done = (i >= items.length);
var value = !done ? items[i++] : undefined; return {
done: done,
value: value
};
}
};
}
var iterator = createIterator([1, 2, 3]);
console.log(iterator.next()); // "{ value: 1, done: false }"
console.log(iterator.next()); // "{ value: 2, done: false }"
console.log(iterator.next()); // "{ value: 3, done: false }"
console.log(iterator.next()); // "{ value: undefined, done: true }"
// 後續的所有呼叫返回的結果都一樣
console.log(iterator.next()); // "{ value: undefined, done: true }"
複製程式碼
可以看出,只需要我們不斷執行next
就可以,不需要手動追蹤迭代的位置。比開篇的迴圈簡單了些。
注意: 最後一次迭代返回的
value
, 並不是集合(如上例子中的items
)裡的值,而是undedined
或者函式返回值。 檢視這裡,可以瞭解返回值與result的關係
generator 是什麼
上面雖然是比 for
迴圈簡單了些,但手動寫個 iterator
太麻煩了,所以ES6
推出 generator
,方便建立 iterator
。也就是說,generator
就是一個返回值為 iterator
的函式。
其語法如下:
function* createIterator() {
yield 1;
yield 2;
yield 3;
}
// generators可以像正常函式一樣被呼叫,不同的是會返回一個 iterator
let iterator = createIterator();
console.log(iterator.next().value); // 1
console.log(iterator.next().value); // 2
console.log(iterator.next().value); // 3
複製程式碼
例子很明瞭,*
標明這是個 generators
, yield
用來在呼叫 next
時返回 value
。
方法裡的 generator
, 可以簡寫:
let o = {
createIterator: function* (items) {
for (let i = 0; i < items.length; i++) {
yield items[i];
}
}
};
// 等同於
let o = {
*createIterator(items) {
for (let i = 0; i < items.length; i++) {
yield items[i];
}
}
};
複製程式碼
可以看到,建立 generator
函式有三種方法
-
函式宣告
function* createIterator() {...} 複製程式碼
-
函式表示式
const createIterator = function* () {...} 複製程式碼
-
物件裡的簡寫方式
let o = { *createIterator(items) { ... } }; 複製程式碼
可以認為,就是在函式關鍵字 function
和函式名之間加 *
, 只不過,不同場景下關鍵和函式名可以省略罷了
[function] * [name]() {}
// * 可以靠近關鍵字也可以靠近函式名,或兩不靠近,都可以
複製程式碼
注意點:
-
需要注意的是,
yield
不能跨函式:function* createIterator(items) { items.forEach(function (item) { // 語法錯誤 yield item + 1; }); } 複製程式碼
-
箭頭函式不能用做
generator
iterable 和 for-of 迴圈
iterable
和 iterator
緊密是相關的,有個叫 iterable
的物件。它有個 Symbol.iterator
屬性,其值是個 generator
函式。
用程式碼描述的話,iterable
長這樣:
let collection = {
items: [],
*[Symbol.iterator]() {
for (let item of this.items) {
yield item;
}
}
};
複製程式碼
ES6
裡的集合如陣列、set、map甚至字串,都是 iterable
。generator
建立的 iterator
都被預設新增了 Symbol.iterator
,所以,它們也是 iterable
。
所有的 iterable
, 也都可以使用 for-of
迴圈。
for-of 迴圈
雖然有了 iterator
,只要呼叫它的next
方法,就可以迭代了。但是,每次手動呼叫也太麻煩了,所以 ES6
推出了 for-of
迴圈:
const colors = ["red", "green", "blue"];
for (let color of colors) {
console.log(color);
}
複製程式碼
對比開篇的 for
迴圈:
const colors = ["red", "green", "blue"];
for (var i = 0, len = colors.length; i < len; i++) {
console.log(colors[i]);
}
複製程式碼
可以看出,for-of
迴圈
- 不用追蹤迭代位置;
- 不用判斷迴圈終止條件;
只需要宣告變數活動每次迭代的值即可,簡潔明瞭。
注意:for-of
只能用在 iterable
上,用其他物件上會報錯。
內建的 iterator
iterator
是 ES6
裡一個重要的部分,一些內建的資料型別都內建了 iterator
,方便開發。
這裡簡要介紹下有 iterator
的資料型別。雖然簡單,但不是不重要。
集合的 iterator
ES6
裡的集合有三類:
- 陣列
- set
- map
他們下面的 iterator
有:
entries():
返回迭代結果為鍵值對的 iterator
;
values():
返回迭代結果為集合裡的值的 iterator
;
keys():
返回迭代結果為集合裡的 key
的 iterator
;
下面以 map
為例:
let tracking = new Map([
['name', 'jeyvie'],
['pro', 'fd'],
['hobby', 'programming']
]);
for (let entry of tracking.entries()) {
console.log(entry);
}
// ["name", "jeyvie"]
// ["pro", "fd"]
// ["hobby", "programming"]
for (let key of tracking.keys()) {
console.log(key);
}
// name
// pro
// hobby
for (let value of tracking.values()) {
console.log(value);
}
// jeyvie
// fd
// programming
複製程式碼
其中 set
裡的 key
和 value
相等,都是 set
裡的值。陣列的 key
是其下標 index
, value
是裡面的值。
另外,各集合都有預設的 iterator
供 for-of 呼叫。 其執行結果,反應的是集合如何被初始化的。
如上例子 tracking
:
for (let value of tracking) {
console.log(value);
}
// ["name", "jeyvie"]
// ["pro", "fd"]
// ["hobby", "programming"]
複製程式碼
對應這 Map
例項化是出入的子項 -- 有兩個元素的陣列。
其他的set
、 陣列與之類似,不贅述了。
需要留意,經驗證
chrome v65
和node v8.0
裡陣列都沒有values()
。也許是因為對陣列而言,它比較冗餘吧。
字串的 iterator
字串裡 iterator
,理解一句話就可以: 是基於 code point
而不是code unit
迭代的。比較一下兩個例子:
例子1:
基於 code unit
var message = "A ? B";
for (let i=0; i < message.length; i++) {
console.log(message[i]);
}
// A
// (空)
// �
// �
// (空)
// B
複製程式碼
例子2:
基於 code point
var message = "A ? B";
for (let c of message) {
console.log(c);
}
// A
// (空)
// ?
// (空)
// B
複製程式碼
基於code point
可以理解為基於字元, 關於它是什麼,可以檢視字串那章。
NodeList 的 iterator
以前要迭代 NodeList
(元素集合),需要用 for
迴圈:
// 可以這樣
for (var i=0, len=NodeList; i<len; i++) {
var el = NodeList[i]
// ....
}
複製程式碼
ES6
可以直接這樣:
for (let el of NodeList) {
// el 就是集合裡的每個元素
}
複製程式碼
另外,現在
NodeList
也有forEach
方法了
spread操作符
展開操作符 ...
和 iterable
展開操作符可以用在所有的 iterable
上,並根據該 iterable
的預設 iterator
決定取哪個值。
比如我們可以將字串轉為陣列:
[...'A?B']
// ['A', '?', 'B']
複製程式碼
結語
這裡以 for
迴圈的問題開始, 講述 ES6
裡怎麼解決這個問題,由此引出 generator
, 然後帶出了 iterator
和 iterable
, 以及操作 iterable
的 for-of
。 最後講了 ES6
裡結合 for-of
, 能使內建集合
的操作起來更加方便。
當然了,for
迴圈問題也許並不是最初 generator
產生的原因,generator
也不只是解決了 for
迴圈這一個問題,它還有更多的高階功能,在實際中的作用更大,後面我會繼續發表出來。
那我為什麼要寫這篇文章,還成了個“標題黨”呢?因為我好奇,我不只是想知道一項技術當前到底怎麼用,還是知道它為何出現,想知道其來龍去脈。這樣,一方面能更瞭解了技術,另一反面,也更能瞭解技術發展的趨勢,這樣,才能可能看得清前方啊,不至於東奔西撞,走那麼多彎路。
最後,一切都是拋磚引玉,歡迎大家批評指正!
另外,generator
高階用法篇 怎麼往 Generator 裡拋個錯? 已經寫完,歡迎方家指正。