JS讲透迭代器-自定义篇

711 阅读5分钟

自定义迭代器

上篇文章讲了迭代器的基本运用和协议规范,不熟悉的话,快去看看,超好懂的。 👉 JS一篇文章讲透迭代器-超好懂

说了那么多原理,就是为自定义做准备的

先整理下迭代器的协议

  1. 迭代器是迭代可迭代对象的关键,而它是从迭代对象中获取的。迭代对象有个Symbol.iterator属性,这个属性指向一个函数,调用该函数,就可以得到对应的迭代器
  2. 拿到迭代器之后,可以通过调用迭代器next的方法,来逐个遍历每个元素
  3. 迭代过程返回的数据结构为{done,value},其中的done表示是否迭代结束,如果done === false,即没有迭代结束;如果done === true,即迭代结束。其中的value,是每次迭代返回的真正的值。

OK,我们来看具体代码实现

class Counter {
  constructor(count) {
    this.count = count;
  }
  
  [Symbol.iterator]() {
    let index = 0;
    return {
      next: () => {
        if (index < this.count) {
          return {
            done: false,
            value: index++
          }
        } else {
          return {
            done: true,
            value: null
          }
        }
      }
    }
  }
}

这里声明了一个class,构造函数接收了个参数,表示待会迭代多少次。关键点在Symbol.iterator,这个属性指向一个函数,调用这个函数就能得到对应的迭代器。

迭代器中有个next属性,它也是一个函数,并且调用之后会返回一个{done,value}。注意其中的done的值,迭代没有结束的时候为false,迭代结束了为true。

应该不难理解我为啥要这么写吧,全是按照迭代器协议来写的

来看看这个自定迭代器是否生效:

const counter = new Counter(4);
for (const c of counter) {
  console.log(c);
}
// 0
// 1
// 2
// 3

实例化的时候,传进去’ 4 ‘,之后的迭代也迭代4次,并且输出的值是每次迭代的序号。

完全符合预期😄

const setCounter = new Set(counter);
console.log(setCounter);
// Set(4) { 0, 1, 2, 3 }

用可迭代对象来创建一个set实例化对象,输出的结果显示,set中的每个元素都是迭代对象的每个元素

结果也是符合预期😄

再来做几个骚操作:

const arrayCounter = [...counter];
console.log(arrayCounter);
// [ 0, 1, 2, 3 ]

const [a, b] = counter;
console.log(a);
console.log(b);
// 0
// 1

数组解构和拓展操作符都是基于可迭代对象的

读者也可以用自定义迭代器尝试其他的原生语言结构,有助于理解记忆上面JS中涉及到迭代器的应用

迭代器加餐

其实,你会了上面的部分,就可以应付开发中的迭代器常见问题。下面说点迭代器的边角料

迭代器特性

我们来观察一段代码

const array = ['z', 'o', 'r', 'a'];
const iteratorArray1 = array[Symbol.iterator]();
const iteratorArray2 = array[Symbol.iterator]();

console.log(iteratorArray1.next());
console.log(iteratorArray1.next());

console.log(iteratorArray2.next());

// { value: 'z', done: false }
// { value: 'o', done: false }
// { value: 'z', done: false }

这里用了原生的迭代器做例子。显示从array中获取了两个迭代器,分别开始迭代。可以看到这两个迭代器是互不影响的。

这时iteratorArray2迭代器现在读取的是第一个元素,如果再调用next,读取到的值必然为’o‘。不过在这之前,对数组做点修改

array.splice(1, 0, 'zenos');

console.log(array);
// [ 'z', 'zenos', 'o', 'r', 'a' ]

在array的第二个位置,插入了一个’zenos‘的字符串。

可以猜下iteratorArray2接下来读到的元素是什么

console.log(iteratorArray2.next());
// { value: 'zenos', done: false }

是的,数组上的修改,立马就能在迭代器中反映出来。这就是迭代器的实时性。

总结一下迭代器的特性:独立性、实时性、一次性


读者:这个一次性是指什么?

作者:一次性就是指迭代器只能遍历迭代对象一次,不能回头遍历第二次

读者:哦,迭代器中没有回头的API,有的只是不断往下的next

作者:是的


迭代器的中断

for-of遍历过程中,可以通过break;或者throw中断遍历。中断遍历会调用迭代器的return方法。这是迭代器的又一个API。

class Counter {
  constructor(count) {
    this.count = count;
  }

  [Symbol.iterator]() {
    let index = 0;
    return {
      next: () => {
        if (index < this.count) {
          return {
            done: false,
            value: index++
          }
        } else {
          return {
            done: true,
            value: null
          }
        }
      },
      return: () => {
        console.log('exist early');
        return {
          done: true
        }
      }
    }
  }
}

上面自定义迭代器中,定义了一个return函数,当其被调用的时候,就会输出"exist early"

const counter = new Counter(4);
for (const c of counter) {
  console.log(c);
  if (c === 2) {
    break;
  }
}
// 0
// 1
// 2
// exist early

结果符合预期😁

再来一个骚操作:

const [a, b] = counter;

console.log(a);
console.log(b);
// exist early
// 0
// 1

数组的解构也是一个迭代的过程,而这里只迭代了前两个就中断了迭代

但是迭代器中断之后,迭代器是否被关闭了呢?

const array = [1, 2, 3, 4];
const iteratorArray = array[Symbol.iterator]();

for (const item of iteratorArray) {
  console.log(item);
  if (item === 2) {
    break;
  }
}

console.log(iteratorArray.next());
// 1
// 2
// { value: 3, done: false }

要看懂上面的代码,还需要知道一个概念:原生的迭代器本身也有Symbol.iterator属性,调用这个方法也会返回迭代器,这个迭代器就是自己。

const array = [1, 2, 3, 4];
const iteratorArray = array[Symbol.iterator]();
const iteratorArray2 = iteratorArray[Symbol.iterator]();

//迭代器的Symbol.iterator函数,返回了自己
console.log(iteratorArray === iteratorArray2);

这也就是为什么for-of中把迭代对象的迭代器作为迭代的数据源也是可以的

然后迭代iteratorArray的过程中,调用了break中断了迭代。在后面又调用了迭代器的next,可以看到输出结果恰好是中断点的下一个元素。

也就是说中断迭代器,并不会一定会导致迭代器关闭,这是不一定的。

for-of迭代数据的时候,会调用对象的Symbol.iterator方法。

而正是因为迭代器的Symbol.iterator函数返回了自己,所以就能通过再次调用iteratorArray.next来证明 “ 中断不会导致迭代器关闭 ” 这个结论。


读者:你为了说清楚这个结论,不惜引入了一个新概念“迭代器的Symbol.iterator函数返回了自己”

作者:是啊,这是没有办法的事情啊。你看懂了吗

读者:嗯,还得多想想


除了break可以中断迭代过程,throw也可以办得到,这个就留给读者自己去尝试吧

总结

  1. 自定义迭代器
  2. 迭代器特性:独立性,实时性,一次性
  3. 迭代器的Symbol.iterator函数会返回自己
  4. 迭代过程被中断后,并不一定会导致迭代器的关闭
  5. 有不明白的地方,留言告诉我