你需要知道的迭代协议与生成器

242 阅读5分钟

Intro

首先关于这些名称可能很多人都很模糊不清,尽管都是有关迭代遍历但并不清楚彼此的定义和区别,那现在我们就慢慢的解释清楚。

terminology

那我们优先从每一个术语称呼开始吧,来看看这些术语对应的中文名:

  • iterable object: [可]迭代对象

  • Iteration protocols: 迭代协议

  • generator:生成器

Iteration protocols - 迭代协议

细心的同学大概已经发现 Iteration protocols 是复数。是的在协议里其实有2种协议分别是:

  • Iteration protocols

    • iterator protocol:迭代器协议

    • iterable protocol:可迭代协议

what are they?

在以往的JavaScripts中已经存在许多对集合类型对迭代方法,例如:for .. in, for 循环, map(), forEach()... 这些语法或者API都是有JavaScript内部实现如何进行迭代集合。例如:

  1. for .. in语法就是遍历所有non-Symbol的可枚举属性
  2. map()API 就是对数组对索引依次遍历得到一个新数组;

那么我们如何对一个变量实现我们自定义的迭代方式呢,这就需要依靠迭代协议。其实在我看来 迭代器协议 与 可迭代协议 是非常类似的以至于初探时后很难弄清,下面我们分别来看看2个协议。

iterator protocol - 迭代器协议

image 😰😰😰

其实 迭代器协议 就是一个对象上定义了一个next属性,而这个next属性的定义有一定的要求,满足的话这就是一个实现了迭代器协议的对象,也被叫做 iterator : 迭代器

那么这个next属性有声明要求呢?

  1. 首先 next 是一个方法,它不接受任何参数传入;
  2. 其次调用next这个方法会返回一个对象,它包含2个属性 value & done,其中 value代表本次迭代得到的数据而 done用来表示迭代是否结束。例如: image 以上 iterator 变量就是一个实现了迭代器协议的迭代器对象。

✌️PS:迭代器协议只是一种协议它指定了一个对象的迭代行为控制(每次迭代返回值、迭代终点),但如何自动迭代运行还需要自己编码实现(不然你得完全编码不停的iterator.next()、iterator.next()、iterator.next()),且每次迭代的上下文你得自己想办法保留。

iterable protocol - 可迭代协议

image 🤬🤬🤬

其实可迭代协议是一个对象是拥有 @@iterator 属性,而这个属性键的定义来自 Symbol.iterator, 同样@@iterator 属性有一定要求,满足要求就实现了可迭代协议。

这些要求分别是:

  1. [Symbol.iterator](key name)属性是一个方法,且不接受任何参数;
  2. 方法返回一个对象,这个对象就是迭代器协议对象。例如: image

这就是两种迭代协议对内容与区别,那么说完迭代协议我们可以来谈谈iterable object。

iterable object - [可]迭代对象

可迭代对象是对象上实现了 iterable protocol - 可迭代协议 的对象,且可以使用build-ins语法进行迭代,例如 for (let i in iterable)[...iterable]。 ⚠️注意: 使用这些build-ins语法必须是对象上实现了可迭代协议不是迭代器协议,否则对对象迭代将会抛出异常:

Uncaught TypeError: object is not iterable (cannot read property Symbol(Symbol.iterator))
    at <anonymous>:1:1

目前有很多JavaScript内置对数据集合已经实现了迭代器协议,有:

  1. Array
const iterable = [10, 20, 30];

for (const value of iterable) {
  console.log(value); // 10 20 30
}
  1. String
const iterable = 'boo';

for (const value of iterable) {
  console.log(value);  // 'b' 'o' 'o'
}
  1. TypedArray
  2. Map & Set
  3. fn arguments
  4. DOM collections

到此你可能发现了要实现自定义迭代行为在写法上还是很复杂对,并且这里存在两种迭代协议,不同的库或者工具可能选用某一种方法实现迭代对象的行为,那么就会可能造成不兼容。但由于2中协议期本质又是十分类似所有我们可以创造一个同时满足迭代器协议和可迭代协议的对象,它类似:

var myIterator = {
    next: function() {
        // ...
    },
    [Symbol.iterator]: function() { return this }
}

这样看起来还是很复杂,于是有了我们最后要说的 generator

generator - 生成器

generator对象generator函数 返回,它既符合[可]迭代协议,又符合迭代器协议,就像刚刚那种模版写法。

它的写法如下:

// 生成器函数
function* gen() { 
  yield 1;
  yield 2;
}

// 生成器对象
const g = gen();

JavaScript支持了生成器语法我们就可以更快的实现自定义的迭代对象了,例如上面的一个例子我用生成器实现是这样的: image

具体的 generator 语法再次不再过多解释,这就是 generator 与 itera... 之间的关系。

⚠️注意: 生成器对象不要重复使用 这句话什么意思,我们先来看一个MDN例子🌰:

const gen = (function *(){
  yield 1;
  yield 2;
  yield 3;
})();
for (const o of gen) {
  console.log(o);
  break;  // Closes iterator
}

// The generator should not be re-used, the following does not make sense!
for (const o of gen) {
  console.log(o); // Never called.
}

以上代码我们可以知道 generator对象 就像是一个一次性消费品(一次性筷子🥢)被迭代行为操作一次后将不会再次进行迭代。


基本上所有的东西就说完了,在补充说明最后一点东东

  1. 有了自定义迭代那么如何实现 迭代流 实现 非阻塞代码呢?在很早以前TJ大佬有实现一个库CO就是干这件事情的该库在社区也比较流行。
  2. 迭代器是一个很好去写异步代码的方式,但在 es2017 async/await 语法糖的引入,是的异步代码的编写与阅读更加方便。