什麼是協定
我們遇到過很多協定:http協定,rpc協定,離婚協定等等,那麼究竟什麼是協定?首先,協定必定有兩方,其次兩方都要遵守一定的格式。接下來聊聊我們今天的主角--疊代協定。
使用疊代協定我們可以做什麼
在 vue 模闆的清單渲染中,有這樣的文法:
<li v-for="item in list"/>
<li v-for="(item,idx) in list"/>
在疊代中擷取一個 idx 很友善,但是在 js 的 for ... of... 循環中,我們可以拿到數組中的一項,但是得到索引卻要費一番功夫。而傳統的基于索引的循環,則要多寫一些代碼。那麼有沒有方法,讓我們在 for... of 循環中,既能拿到數組的項,又能拿到索引呢?如下所示:
for (let [item, idx] of list) { ... }
答案是肯定的,就是要借助今天的主角,疊代協定了。
疊代器協定和可疊代對象
疊代器協定(iterator protocol) 定義了一種标準的方式來産生一個有限或無限序列的值,并且當所有的值都已經被疊代後,就會有一個預設的傳回值。當一個對象隻有滿足下述條件才會被認為是一個疊代器,它實作了一個 next() 的方法,而 next 方法會傳回這樣一個對象:{value, done},value 表示本次疊代的值,done 表示疊代是否結束。根據這個協定,我們可以寫一個疊代器:
let iterator = {
from: 1,
to: 10,
next() {
if (this.from < 10) {
return {done: false, value: this.from++}
}
return {done: true}
}
}
于是我們可以手動調用 next()來疊代:
iterator.next()
// {done: false, value: 1}
iterator.next()
// {done: false, value: 2}
iterator.next()
// {done: false, value: 3}
或者使用 while 循環來疊代:
while(true) {
let next = iterator.next()
if(next.done) break // 疊代結束
console.log(next.value)
}
上面的代碼盡管實作了疊代,卻不能使用 for .. of .. 或者
[...]
這樣的内置文法來疊代。
for (let i of iterator) { console.log(i) } // Uncaught TypeError: iterator is not iterable
要實作更自然的疊代方式,我們還需要了解可疊代對象協定:** 為了變成可疊代對象, 一個對象必須實作 @@iterator 方法, 意思是這個對象(或者它原型鍊 prototype chain 上的某個對象)必須有一個名字是 Symbol.iterator 的方法 **
這個也很好了解,因為 for...of 這樣的文法是為可疊代對象設計的。那麼什麼是可疊代對象呢?打個比方,假如你想 35 歲以後跑滴滴,那麼首先你必須有一輛車,這時你就被稱為
可跑滴滴的人
。同理,如果一個對象有了 Symbol.iterator 的方法,而且這個方法調用後會傳回一個疊代器,這時,這個對象就成了一個可疊代對象。下面我們來看看怎麼使一個普通對象變成可疊代的對象。
// 普通對象
let obj = {
a: 'x',
b: 'y',
c: 'z'
}
實作 Symbol.iterator 就可以将普通對象變成可疊代對象:
let obj = {
a: 'x',
b: 'y',
c: 'z'
}
obj[Symbol.iterator] = function() {
let _keys = Object.keys(this)
let _idx = 0
return {
next() {
let key = _keys[_idx++]
if (!key) {
return {done: true}
}
return {
value: [key, this[key]],
done: false
}
}
}
}
這時就可以使用 for...of... 來周遊 obj 了ð:
for (let [k, v] of obj) {console.log(k, v)}
了解了可疊代對象,我們就可以使前面的 iterator 變成一個可疊代對象,然後用 for...of 來疊代了:
iterator[Symbol.iterator] = function() { return this }
由于 iterator 本身就是一個疊代器,使它成為一個可疊代對象隻需要為它增加一個方法,這個方法什麼也不需要做,隻需要 return this 即可。另外,這也不是我們的創新,因為 String.prototype.matchAll 已經這麼做了:
iter = '12345678'.matchAll(/\d/g)
iter.next() // {value: Array(1), done: false} 說明 iter 是一個疊代器
[...iter] // 沒有報錯,說明 iter 也是一個可疊代對象
生成器函數
如果你覺得為了要使一個對象變的可疊代,要自己去理清楚什麼 next,done 很麻煩,那麼生成器函數就是為你準備的。因為它會傳回一個對象,** 同時實作了疊代器協定和可疊代對象協定 **:
function* generator() {
yield 1;
}
let iter = generator();
iter.next() // {value: 1, done: false}
iter.next() // {value: undefined, done: true}
你一樣可以使用 for...of... 來疊代 iter:
let iter = generator();
for (let [k, v] of iter) { console.log(k, v) }
内置的疊代對象
我們知道可以使用 for...of 來疊代數組,字元串這些對象,那是因為 String, Array, TypedArray, Map, Set 都是可疊代對象,他們的原型都實作了 Symbol.iterator 方法。文章一開始,我們希望對一個數組疊代時,除了拿到它的值以外,還可以拿到他的索引,那我們就需要對 [Symbol.iterator] 方法進行重載修改, 我們可以實作一個 reload 方法來修改數組的 Symbol.iterator 方法:
function reload(arr) {
if (Array.isArray(arr)) {
arr[Symbol.iterator] = function*() {
for (let i = 0; i < this.length; i++) {
yield [this[i], i]
}
}
}
}
var arr = ['x', 'y', 'z']
reload(arr)
for (let [item, idx] of arr) {
console.log(item, idx)
}
上面的實作方法會破壞數組的原本性質,這無異于給接管你代碼的人埋坑。我們可以采用更溫和的方法, 通過一個生成器函數來實作:
function* withIndex (arr) {
let i = 0;
while(i < arr.length - 1) {
yield [arr[i], i];
i++;
}
}
var arr = ['x', 'y', 'z'];
for (let [v, i] of withIndex(arr)) {
console.log(v, i);
}
最後一個問題:為什麼不直接修改 Array.prototype[Symbol.iterator]方法呢?因為解構也用到了疊代協定,那不是我們想要的結果ð。有興趣的同學可以試試直接修改原型上的方法。
上面講的全部是同步疊代的内容,随着ES的發展,後來又出現了一步疊代,疊代協定和promise又能擦出怎樣的火花呢,可以看這篇:https://2ality.com/2016/10/asynchronous-iteration.html
武漢加油!本文完。
參考: https://zh.javascript.info/iterable
cnblogs-md-editor編輯器,用Markdown寫部落格就用它