ES6 Generator 教程

1,285 阅读4分钟

原文地址: Understanding Generators in ES6 JavaScript with Examples

简介

es6 新增了生成器函数,以便更好地使用函数和迭代器。generator 是一个函数,函数可以在执行的过程中停止,记住该状态,下次继续执行的时候从该状态开始。简单来说,generator是一个有迭代器功能的函数。

async/await 是generator的语法题。

generator 跟 iterator 密切相关。

举一个简单的例子来理解generator: 假如你正在看一本书,突然门铃响了,是你叫的外卖到了,这个时候你会把书签夹在你正在阅读的页面,然后合上书本,起身开门。吃完外卖你重新打开书本,可以从有标签的那页开始读起,而不用从第一页开始读。这大概就是generator的思想。

什么是generator

下面这个函数是从头开始执行直到结束的,想让它停止执行的唯一方法是return或者抛出错误,重新开始时则又再次重头执行:

function normalFunc() {
  console.log('I')
  console.log('cannot')
  console.log('be')
  console.log('stopped.')
}

generator 则可以随时停止执行,再次开始时从原来停止的位置开始。generator函数返回一个对象,该对象可以调用next()方法,每次调用会返回value和done。generator函数有可能永远都不会结束。

generator vs function

创建一个generator

function * generatorFunction() { // Line 1
  console.log('This will be executed first.');
  yield 'Hello, ';   // Line 2
  console.log('I will be printed after the pause');  
  yield 'World!';
}
const generatorObject = generatorFunction(); // Line 3
console.log(generatorObject.next().value); // Line 4
console.log(generatorObject.next().value); // Line 5
console.log(generatorObject.next().value); // Line 6
// This will be executed first.
// Hello, 
// I will be printed after the pause
// World!
// undefined

line 3 创建了一个generator对象,generator对象是一个迭代器。
line 4 generator函数开始执行,遇到yield停下来
line 5 generator继续执行,从上一个yield结束开始
line 6 generator继续执行,但此时已经没有代码可以执行了,因此返回{value: undefined, done: true}

function, *, 函数名之间可以有无数给空格。因为generator本质是一个函数,所有可以创建和调用函数的位置,相应的也可以创建和调用generator。

generator没有使用return语句,使用的是yield,每当程序执行到yield语句的时候,就会返回yield语句后面跟着的值。

generator也可以使用return语句,但return语句后面的yield语句会失效:

function *  generatorFunc() {
  yield 'a';
  return 'b'; // Generator ends here.
  yield 'a'; // Will never be executed. 
}

generator实践

  1. 实现可迭代

要让一个对象可迭代,我们需要自己实现next方法,还需要保存当前状态,实现一个类似闭包的函数:

const iterableObj = {
  [Symbol.iterator]() {
    let step = 0;
    return {
      next() {
        step++;
        if (step === 1) {
          return { value: 'This', done: false};
        } else if (step === 2) {
          return { value: 'is', done: false};
        } else if (step === 3) {
          return { value: 'iterable.', done: false};
        }
        return { value: '', done: true };
      }
    }
  },
}
for (const val of iterableObj) {
  console.log(val);
}
// This
// is 
// iterable.

使用generator同样可以实现上述功能:

function * iterableObj() {
  yield 'This';
  yield 'is';
  yield 'iterable.'
}
for (const val of iterableObj()) {
  console.log(val);
}
// This
// is 
// iterable.
  1. 异步代码

执行异步代码可以通过Promise, callback等:

function fetchJson(url) {
    return fetch(url)
    .then(request => request.text())
    .then(text => {
        return JSON.parse(text);
    })
    .catch(error => {
        console.log(`ERROR: ${error.stack}`);
    });
}

也可以使用generator(借助 co.js):

const fetchJson = co.wrap(function * (url) {
    try {
        let request = yield fetch(url);
        let text = yield request.text();
        return JSON.parse(text);
    }
    catch (error) {
        console.log(`ERROR: ${error.stack}`);
    }
});
  1. 无限数据流

我们可以创建一个永远不会结束的generator.如下代码,因为while 条件一直为true,代码可以随时开始执行随时停止。

function * naturalNumbers() {
  let num = 1;
  while (true) {
    yield num;
    num = num + 1
  }
}
const numbers = naturalNumbers();
console.log(numbers.next().value)
console.log(numbers.next().value)
// 1
// 2
  1. 观察者模式

generator可以通过next(val)函数来传参。这时generator会被称为观察者,因为收到值后generator会继续执行。

generator优点

  1. 延迟计算

思考一下我们上面提到的无限数据流,如果是在普通函数下,那么while里面的语句会不停的执行,页面会出现卡顿,内存会被快速消耗。但是在generator语境下,我们只在需要的时候才执行while里面的语句,所以不会存在普通函数产生的问题。

  1. 节约内存

延迟计算的直接结果就是节约内存。

注意事项

  1. generator对象只提供一次性访问,但返回{value: undefined, done: true}之后,无法再重头开始遍历,需要重新创建一个generator对象
  2. generator对象不像数组一样可以任意访问某一个值,generator访问到具体某一个值的时候必定已经访问到该值之前的所有值,所以不能说是随机访问。

generator与协程

协程是一种程序运行的方式,可以用单线程实现,ES6对协程的实现是通过generator。