JS-用生成器(Generator)实现可迭代对象

223 阅读5分钟

什么是生成器(Generator)

生成器相信大家都很熟悉了,先来回顾一个例子

function* ge(x, y) {
  const zeroVar = yield '5'
  console.log('zeroVar: ', zeroVar);
​
  const firstVar = yield x + y;
  console.log('firstVar: ', firstVar);
​
  const secondVar = yield firstVar + x;
  console.log('secondVar: ', secondVar);
​
  return secondVar;
}
​
let temp = null;
const geIterator = ge(10, 20);
​
console.log(temp = geIterator.next());
// { value: '5', done: false }console.log(temp = geIterator.next(temp?.value));
// zeroVar:  5
// { value: 30, done: false }console.log(temp = geIterator.next(temp.value));
// firstVar:  30
// { value: 40, done: false }console.log(temp = geIterator.next(temp.value));
// secondVar:  40
// { value: 40, done: true }console.log(temp = geIterator.next(temp.value));
// { value: undefined, done: true }console.log(temp = geIterator.next(temp.value));
// { value: undefined, done: true }

声明

  1. 在普通函数声明中加一个*,就表示这个是生成器了
  2. 生成器也可以接收参数

API

生成器函数执行过程:

  1. 调用生成器函数,返回生成器对象geIterator,这个时候,函数中并没有任何代码执行
  2. 调用geIterator对象next属性,函数中的代码开始执行,执行到yield停止执行。next函数会返回一个对象,yield后面表达式的值,存于返回对象的value中。

这时候,得到了第一次输出

// { value: '5', done: false }

第一个yield后面的是 ‘5’,所以value的值也是‘5’

  1. 再次调用geIterator对象next属性,并传入一个参数'5'。函数的代码从刚开始位置开始执行,首先yield会将next的参数作为值返回给firstVar变量。所以下面的输出得到的结果为'5'。然后继续让下执行,直到执行下一个yield停止。这样,next函数又得到了第二个yield的返回值。这个返回值是x + y,所以value是30。

这时候,控制台可以看到了第二次和第三次输出

// zeroVar:  5
// { value: 30, done: false }
  1. 下面执行逻辑就是和上面一样
  2. 执行到了return,就表示生成器函数执行结束了。这次next得到的对象中,done的值为true,表示执行结束。并且value的值为生成器return的值。
  3. 再次调用next函数,仍然会得到一个相同结构的对象。done永远为true,而value永远是undefined。

总结一下生成器涉及到的API:next,yield,done,value,return

  1. 调用这个函数后会得到一个对象,这个对象有个next方法。
  2. 第一次调用next,会返回第一个yield后面表达式的值;第二次调用next,会返回第二个yield后面表达式的值,依次类推。
  3. 返回的结构是{done,value},其中done表示是否迭代完成,值为false,表示没有完成;值为true,表示迭代完成。value就是yield后面的值
  4. 也可以给next传递参数,这个参数会作为yield的返回值。如例子中的firstVar的值、secondVar的值。
  5. return的值,会作为迭代结束时候值。

ok,掌握了这些生成器知识,相信你就能写出自己的生成器了,并且也能看懂别人的生成器代码了。下面我们来看看生产的器的应用

生成器与迭代器的关系

你如果看了我迭代器的文章(JS讲透迭代器-自定义篇),相信你很快就能发现生成器和迭代器之间的共同点。

  • 返回结构相同{value, done},并且done代表的含义也是相似的
  • 都是通过next不断获取下一个元素

我们再用迭代器的视角看看生成器。

  • 通过调用next获取下一元素,元素的值就是next返回对象的value属性,而且可以通过对象中的done的值来判断是否遍历结束

是不是很像,如果不考虑生成器内部的执行逻辑,他们简直一模一样

//借用上面的例子
const geIterator = ge(10, 20);

我们把ge叫做生成器函数,geIterator叫做生成器对象。而这个生成器对象就是一个可迭代对象

来看代码

function* ge2() {
  yield 'A';
  yield 'E';
  yield 'C';
  yield 'G';
  return 'end';
}
​
const geIterator2 = ge2();
for (const item of geIterator2) {
  console.log('item: ', item);
}
​
// item:  A
// item:  E
// item:  C
// item:  G

for-of迭代并输出了生长器对象中的每一个yield后面的元素,但是没有输出return的值,因为for-of不会输出done为true的value。

console.log(Symbol.iterator in geIterator2); 
// true

这从另一个角度证明了生成器对象是一个可迭代对象

其实,生成器对象也是一个迭代器,具有迭代器的一次性特征。我们用迭代过的geIterator2,再次调用next方法

console.log(geIterator2.next());
//{ value: undefined, done: true }

可以看到,迭代器不能再迭代了,处于关闭状态。

既然生成器对象是一个迭代器,那就可以自定义一个迭代器制作可迭代对象。

这篇文章讲了什么是自定义迭代器(JS讲透迭代器-自定义篇

const rightSound = {
  [Symbol.iterator]: ge2,
};
​
for (const item of rightSound) {
  console.log(item);
}
// A
// E
// C
// Gconst [a, b] = rightSound;
console.log('a: ', a);
console.log('b: ', b);
// a:  A
// b:  Econsole.log(rightSound);
console.log([...rightSound]);
// { [Symbol(Symbol.iterator)]: [GeneratorFunction: ge2] }
// [ 'A', 'E', 'C', 'G' ]

这里借用了上面例子声明的ge2生成器函数

不知道是否让你大吃一惊呢,生成器还可以这样。更重要的是,一个可迭代对象还可以这样。😁

下篇文章讲生成器与递归

总结

  1. 生成器函数的基本执行逻辑
  2. 生成器和迭代器的关系
  3. 用生成器制作一个可迭代对象
  4. 有不明白的,留言告诉我