天天看點

有趣的疊代協定

什麼是協定

我們遇到過很多協定: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寫部落格就用它