生成器与迭代器

以前的文章 写到了 Generator 与异步编程的关系,其实简化异步编程只是 Generator 的“副业”,Generator 自己却不是为异步编程而存在。git

生成器函数

咱们看 Generator 自身的含义——生成器,就是产生序列用的。好比有以下函数:github

function* range(start, stop) {
  for (let item = start; item < stop; ++item) {
    yield item;
  }
}

range 就是一个生成器函数,它自身是函数能够调用(typeof range === 'function' // true),但又与普通函数不一样,生成器函数(GeneratorFunction)永远返回一个生成器(Generator编程

注:咱们一般所说的 Generator 实际上指生成器函数(GeneratorFunction),而把生成器函数返回的对象称做迭代器(Iterator)。因为感受“生成器函数”返回“生成器”这句话有些拗口,下文沿用生成器和迭代器的说法。segmentfault

迭代器

初次调用生成器实际上不执行生成器函数的函数体,它只是返回一个迭代器,当用户调用迭代器的 next 函数时,程序才开始真正执行生成器的函数体。当程序运行到 yield 表达式时,会将 yield 后面表达式的值做为 next 函数的返回值(的一部分)返回,函数自己暂停执行。数组

const iterator = range(0, 10); // 获取迭代器
const value1 = iterator.next().value; // 获取第一个值 => 0
const value2 = iterator.next().value; // 获取第二个值 => 1

next 返回值是一个对象,包含两个属性 valuedone。value 即为 yield 后面表达式的值,done 表示函数是否已经结束(return)。若是函数 return(或执行到函数尾退出,至关于 return undefined),则 done 为 true,value 为 return 的值。浏览器

for...of 是遍历整个迭代器的简单方式。异步

生成器的用处

上面说到,生成器就是生成序列用的。可是与直接返回数组不一样,生成器返回序列是一项一项计算并返回的,而返回数组老是须要计算出全部值后统一返回。因此至少有三种状况应该考虑使用生成器。异步编程

  • 序列有无限多项,或者调用者不肯定须要多少项

range(0, Infinity) 是容许的,由于生成器没生成一个值就会暂停执行,因此不会形成死循环,能够由调用者选择什么时候中止。函数

注意此时不能使用 for...of,由于迭代器永远不会 donethis

  • 计算每一项耗时较长

若是计算一项的值须要 1ms,那么计算 1000 项就须要 1s,若是不将这 1s 拆分,就会致使浏览器卡顿甚至假死。这时能够用生成器每生成几项就将控制权交还给浏览器,用于响应用户事件,提高用户体验(固然这里更有效的方法是将代码放到 Web Worker 里执行)

  • 节省内存

若是序列很长,直接返回数组会占用较大内存。生成器返回值是一项一项返回,不会一次性占用大量内存(固然生成器为了保留执行上下文比一般函数占用内存更多,可是它是个定值,不随迭代次数增长)

使用生成器实现懒加载的 map、filter

Array#mapArray#filter 是 ES5 引入的(绝对不算新的)两个很是经常使用的函数,前者将数组每一项经过回调函数映射到新数组(值变量不变),后者经过回调函数过滤某些不须要的项(量变值不变),他们都会生成新的数组对象,若是数组自己较长或者写了很长的 map、filter 调用链,就可能形成内存浪费。

这时就能够考虑使用生成器实现这两个函数,很是简单:

function* map(iterable, callback) {
  let i = 0;
  for (const item of iterable) {         // 遍历数组
    yield callback(item, i++, iterable); // 获取其中一项,调用回调函数,yield 返回值
  }
}

function* filter(iterable, callback) {
  let i = 0;
  for (const item of iterable) {         // 遍历数组
    if (callback(item, i++, iterable)) { // 获取其中一项,调用回调函数
      yield item;                        // 仅当回调函数返回真时,才 yield 值
    }
  }
}

能够看到我在代码中写的是“可迭代的”(iterable),而不限于数组(因为实现了 Symbol.iterator 因此数组也是可迭代对象)。好比能够这么用:

const result = map(     // (1)
  filter(               // (2)
    range(1, 10000),    // (3)
    x => x % 2 === 0,
  ),
  x => x / 2,
)
console.log(...result); // (4)

注意,程序在解构运算符 ...result 这一步才真正开始计算 result 的值(所谓的懒加载),并且它的值也是一个一个计算的:

  1. (3)生成迭代器,提供值给(2);(2)提供值给(1)
  2. (1)中的result也是迭代器,在这一步全部函数都没有真正开始执行,由于没有任何代码问他们索要值。
  3. (4)中的扩展运算符对迭代器 result 进行了求值,生成器真正开始执行。
  4. result 的值来自于 (1),因而(1)首先开始执行。
  5. (1)中map函数使用 for...of 遍历(2)提供的迭代器,因而(2)开始执行
  6. (2)中filter函数使用 for...of 遍历(3)提供的迭代器,因而(3)开始执行
  7. (3)中range函数开始执行,循环获得第一个值 1。遇到 yield 关键字,将值 1 输出给(2)
  8. (2)中的 for...of 得到一个值 1,执行函数体。callback 返回 false,忽略之。回到 for...of,继续问(3)索要下一个值
  9. (3)range 输出第二个值 2
  10. (2)中的 for...of 得到一个值 2,执行函数体。callback 返回 true,将值 2 输出给 (1)
  11. (1)中的 for...of 得到一个值 2,执行函数体获得 1。将值 1 输出给(4),console.log 得到第一个参数
  12. (4)检测result尚未结束(done为false),问result索要下一个值。
  13. 回到第 4 步循环,直至(3)中的循环结束函数体退出为止,(3)返回的迭代器被关闭
  14. (2)中 for...of 检测到迭代器已被关闭(done为true),循环结束,函数退出,(2)返回的迭代器被关闭
  15. 同理(1)返回的迭代器被关闭
  16. (4)中解构运算符检测到result已关闭,结构结束。将结构获得的全部值做为 console.log 的参数列表输出

总结一下,代码执行顺序大概是这样:(3) -> (2) -> (1) -> (4) -> (1) -> (2) -> (3) -> (2) -> (3) -> (2) -> (1) -> (4) -> (1) -> (2) -> (3) -> ……

是否是至关复杂?异步函数中“跳来跳去”的执行顺序也就是这么来的。跟递归函数同样,不要太纠结生成器函数的执行顺序,而要着重理解它这一步究竟要作什么事情。

生成器函数的链式调用

这样的代码 map(filter(range(1, 100), x => x % 2 === 0), x => x / 2) 彷佛有些d疼,好像又看到了被回调函数支配的恐惧。虽然有人提出了管道符的提议,但这种 stage 1 的提议被标准接受直至有浏览器实现实在是遥遥无期,有没有其余办法呢?

普通函数能够简单的经过在类中返回 this 实现函数的链式调用(例如经典的 jQuery),可是这点在生成器中不适用。咱们前面说过生成器函数自己永远返回一个迭代器,而生成器中的 return 语句其实是关闭迭代器的标志,return this 实际表明 { value: this, done: true }。生成器中的 return 和普通函数用法相近但实际含义大大不一样。

链式调用须要函数的返回值是个对象,而且对象中包含可链式调用的全部函数。生成器函数返回的迭代器自己就是一个对象,很容易想到改变对象的原型实现。

迭代器有以下原型继承链:

迭代器对象 -> 生成器.prototype -> 生成器.prototype.prototype(Generator) -> Object.prototype -> null

clipboard.png

能够看到,生成器返回的迭代器对象就好像是被生成器 new 出来的同样(可是生成器不是构造函数不能被 new)。可是总之咱们能够经过给生成器函数的 prototype 添加方法实现给迭代器添加方法的效果。实现以下

function* range(start, stop) {
  for (let item = start; item < stop; ++item) {
    yield item;
  }
}

function* map(callback) {
  let i = 0;
  for (const item of this) {
    yield callback(item, i++, this);
  }
}

function* filter(callback) {
  let i = 0;
  for (const item of this) {
    if (callback(item, i++, this)) {
      yield item;
    }
  }
}

[range, map, filter].forEach(x => Object.assign(x.prototype, { range, map, filter }));

// 使用
const result = range(1, 100)
  .filter(x => x % 2 === 0)
  .map(x => x / 2);

console.log(...result);

笔者业(xian)余(de)时(dan)间(teng)使用迭代器实现了几乎全部 ES7 中 Array 的成员方法和静态方法,广而告之,欢迎来喷:https://github.com/CarterLi/q...

相关文章
相关标签/搜索