有趣的迭代协议

机器学习回顾篇(15):集成学习之GDBT

使用迭代协议我们可以做什么

在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 .. 或者 [...]这样的内置语法来迭代。要实现更自然的迭代方式,我们还需要了解可迭代对象协议:为了变成可迭代对象, 一个对象必须实现 @@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)}

生成器函数

如果你觉得为了要使一个对象变的可迭代,要自己去理清楚什么next,done很麻烦,那么生成器函数就是为你准备的。因为它会返回一个迭代器:

function* generator() {
    yield 1;
}
let iter = generator();
iter.next() // {value: 1, done: false}
iter.next() // {value: undefined, done: true}

然后我们把obj的Symbol.iterator替换成一个生成器函数:

obj[Symbol.iterator] = function*() {
    let _keys = Object.keys(this)
    while(_keys.length > 0) {
        let key = _keys.shift()
        yield [key, this[key]]
    }
}

这时,你一样可以使用 for…of…来迭代obj了:

for (let [k, v] of obj) { 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)
}

最后一个问题:为什么直接修改Array.prototype[Symbol.iterator]方法呢?因为解构也用到了迭代协议,那不是我们想要的结果。有兴趣的同学可以试试直接修改原型上的方法

武汉加油!本文完。

上周热点回顾(1.20-1.26)

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享