Skip to content

Generator 函数

简介

Generator 函数是 ES6 提供的一种异步编程解决方案

执行 Generator 函数会返回一个遍历器对象。返回的遍历器对象,可以依次遍历 Generator 函数内部的每一个状态。

Generator 函数是一个普通函数,但有两大特征

  1. function 关键字与函数名之间有一个星号
  2. 函数体内部使用 yield 表达式,定义不同的内部状态
js
function* helloWorldGenerator() {
  yield 'hello'
  yield 'world'
  return 'ending'
}

var hw = helloWorldGenerator()
hw.next()
hw.next()
hw.next()
/*
    {value: 'hello', done: false} 
    {value: 'world', done: false} 
    {value: 'ending', done: true}
*/

function 关键字与函数名之间的星号位置,没有明确规定(推荐第三种)

js
function * foo(x, y) { ··· }
function *foo(x, y) { ··· }
function* foo(x, y) { ··· }
function*foo(x, y) { ··· }

Generator 调用方式

  1. 调用 Generator 函数后,该函数并不执行,返回值不是函数运行结果,而是一个指向内部状态的指针对象。

  2. 需要调用 指针对象的 next 方法,使指针移向下一个状态

  3. 每次调用 next 方法,内部指针就从函数头部或上一次停下来的地方开始执行,直到遇到下一个 yield 表达式(或 return 语句)

Generator 函数是分段执行:yield 表达式是暂停执行的标记,而 next 方法可以恢复执行

next 方法

返回值:含 value 和 done 两个属性的对象

  • value:当前的内部状态的值,是 yield 表达式后面那个表达式的值
  • done:是否遍历结束(布尔值)

运行逻辑

  1. 遇到 yield 表达式,就暂停执行后面的操作
  2. 下一次调用 next 方法时,再继续往下执行,直到遇到下一个 yield 表达式
  3. 没有再遇到新的 yield 表达式,就一直运行到函数结束,直到 return 语句为止,并将 return 语句后面的表达式的值,作为返回的对象的 value 属性值
  4. 没有 return 语句,则返回的对象的 value 属性值为 undefined

参数

next 方法:可以带一个参数,该参数就会被当作上一个 yield 表达式的返回值

js
function* f() {
  for (var i = 0; true; i++) {
    var reset = yield i
    if (reset) {
      i = -1
    }
  }
}

var g = f()

g.next() // { value: 0, done: false }
g.next() // { value: 1, done: false }
g.next(true) // { value: 0, done: false }

Generator 函数从暂停状态到恢复运行,上下文状态(context)是不变的。

向 next 方法传参,可以在 Generator 函数开始运行之后,向函数体内部注入值。

js
function* foo(x) {
  var y = 2 * (yield x + 1)
  var z = yield y / 3
  return x + y + z
}

var a = foo(5)
a.next() // Object{value:6, done:false}
a.next() // Object{value:NaN, done:false}
a.next() // Object{value:NaN, done:true}

var b = foo(5)
b.next() // { value:6, done:false }
b.next(12) // { value:8, done:false }
b.next(13) // { value:42, done:true }

传值是把上一个 yield 表达式值替换为所传参数

next 方法参数:表示上一个 yield 表达式的返回值,所以第一次使用 next 方法时,传递参数是无效的

V8 引擎直接忽略第一次使用 next 方法时的参数

要使第一次 next 传参有效,Generator 函数外包一层

js
function wrapper(generatorFunction) {
  return function (...args) {
    let generatorObject = generatorFunction(...args)
    generatorObject.next()
    return generatorObject
  }
}

const wrapped = wrapper(function* () {
  console.log(`First input: ${yield}`)
  return 'DONE'
})

wrapped().next('hello!')
// First input: hello!

yield 表达式

返回值:默认返回 undefined,可通过向 next 传参改变

Generator 函数返回的遍历器对象,只有调用 next 方法才会遍历下一个内部状态,未执行前处于暂停状态。yield 表达式就是暂停标志。

yield 表达式为惰性求值

js
function* gen() {
  yield 123 + 456
}

当指针移向 123+456 这行时,才会求值

yield 和 return

相似点:返回紧跟在语句后面的表达式的值

区别:遇到 yield,函数暂停执行,下一次再从该位置继续向后执行,而 return 语句直接结束

Generator 函数可以不用 yield 表达式,变成暂缓执行函数

js
function* f() {
  console.log('执行了!')
}

var generator = f()

setTimeout(function () {
  generator.next()
}, 2000)

yield 只能在 Generator 函数使用,其他地方报错

js
(function (){
  yield 1;
})()
// SyntaxError: Unexpected number

例子:打平数组

js
var arr = [1, [[2, 3], 4], [5, 6]]

var flat = function* (a) {
  var length = a.length
  for (var i = 0; i < length; i++) {
    var item = a[i]
    if (typeof item !== 'number') {
      yield* flat(item)
    } else {
      yield item
    }
  }
}

for (var f of flat(arr)) {
  console.log(f)
}
// 1, 2, 3, 4, 5, 6

不能使用 forEach,因为 yeild 只能在 Generator 函数中使用

yield 表达式用在另一个表达式之中,必须放在圆括号里面。

js
function* demo() {
  console.log('Hello' + yield); // SyntaxError
  console.log('Hello' + yield 123); // SyntaxError

  console.log('Hello' + (yield)); // OK
  console.log('Hello' + (yield 123)); // OK
}

yield 表达式用作函数参数或放在赋值表达式的右边,可以不加括号

js
function* demo() {
  foo(yield 'a', yield 'b') // OK
  let input = yield // OK
}

与 Iterator 关系

Generator 函数是遍历器生成函数,因此可以把 Generator 赋值给对象的 Symbol.iterator,让该对象具有 Iterator 接口

js
var myIterable = {}
myIterable[Symbol.iterator] = function* () {
  yield 1
  yield 2
  yield 3
}
;[...myIterable] // [1, 2, 3]

Generator 函数执行后,返回一个遍历器对象。该对象也具有 Symbol.iterator,执行后返回自身

js
function* gen() {
  // some code
}

var g = gen()

g[Symbol.iterator]() === g
// true

for...of

for...of 循环:自动遍历 Generator 函数运行时生成的 Iterator 对象

next 方法返回对象 done 为 true,循环终止,并且不包含该对象

原生 JavaScript 对象遍历

直接编写遍历函数

js
function* objectEntries(obj) {
  let propKeys = Reflect.ownKeys(obj)

  for (let propKey of propKeys) {
    yield [propKey, obj[propKey]]
  }
}

let jane = { first: 'Jane', last: 'Doe' }

for (let [key, value] of objectEntries(jane)) {
  console.log(`${key}: ${value}`)
}
// first: Jane
// last: Doe

遍历函数添加到 Symbol.iterator

js
jane[Symbol.iterator] = objectEntries

for (let [key, value] of jane) {
  console.log(`${key}: ${value}`)
}

扩展运算符(...)、解构赋值和 Array.from 方法内部调用的,都是遍历器接口。这意味着,它们都可以将 Generator 函数返回的 Iterator 对象,作为参数。

js
function* numbers() {
  yield 1
  yield 2
  return 3
  yield 4
}

// 扩展运算符
;[...numbers()] // [1, 2]

// Array.from 方法
Array.from(numbers()) // [1, 2]

// 解构赋值
let [x, y] = numbers()
x // 1
y // 2

// for...of 循环
for (let n of numbers()) {
  console.log(n)
}
// 1
// 2

Generator.prototype

throw()

throw 方法:在函数体外抛出错误,然后在 Generator 函数体内捕获。接受一个参数,该参数会被 catch 语句接收

js
var g = function* () {
  try {
    yield
  } catch (e) {
    console.log('内部捕获', e)
  }
}

var i = g()
i.next()

try {
  i.throw('a')
  i.throw('b')
} catch (e) {
  console.log('外部捕获', e)
}
// 内部捕获 a
// 外部捕获 b

第一次抛出错误,通过内部捕获。第二次抛出错误,try..catch 已执行,错误被外部捕获

与全局 throw 区别:

全局 throw 抛出的错误,只能由外部捕获,不能由 Generator 内部捕获

js
var g = function* () {
  while (true) {
    try {
      yield
    } catch (e) {
      if (e != 'a') throw e
      console.log('内部捕获', e)
    }
  }
}

var i = g()
i.next()

try {
  throw new Error('a')
  throw new Error('b')
} catch (e) {
  console.log('外部捕获', e)
}
// 外部捕获 [Error: a]

Generator 函数内部没有部署 try...catch 代码块,那么 throw 方法抛出的错误,将被外部 try...catch 代码块捕获

Generator 函数内部和外部,都没有部署 try...catch 代码块,那么程序将报错,直接中断执行

js
var gen = function* gen() {
  yield console.log('hello')
  yield console.log('world')
}

var g = gen()
g.next()
g.throw()
// hello
// Uncaught undefined

throw 方法抛出的错误要被内部捕获,前提是必须至少执行过一次 next 方法。等同于通过启动 Generator

js
function* gen() {
  try {
    yield 1
  } catch (e) {
    console.log('内部捕获')
  }
}

var g = gen()
g.throw(1)
// Uncaught 1

throw 方法被捕获以后,会附带执行下一条 yield 表达式。也就是 throw 抛出的错误,不会影响下次遍历

js
var gen = function* gen() {
  try {
    yield console.log('a')
  } catch (e) {
    // ...
  }
  yield console.log('b')
  yield console.log('c')
}

var g = gen()
g.next() // a
g.throw() // b
g.next() // c

函数内捕获错误的好处:多个 yield 表达式,可以只用一个 try...catch 代码块来捕获错误

Generator 执行过程中抛出错误,且没有被内部捕获,就不会再执行下去了

js
function* g() {
  yield 1
  console.log('throwing an exception')
  throw new Error('generator broke!')
  yield 2
  yield 3
}

function log(generator) {
  var v
  console.log('starting generator')
  try {
    v = generator.next()
    console.log('第一次运行next方法', v)
  } catch (err) {
    console.log('捕捉错误', v)
  }
  try {
    v = generator.next()
    console.log('第二次运行next方法', v)
  } catch (err) {
    console.log('捕捉错误', v)
  }
  try {
    v = generator.next()
    console.log('第三次运行next方法', v)
  } catch (err) {
    console.log('捕捉错误', v)
  }
  console.log('caller done')
}

log(g())
// starting generator
// 第一次运行next方法 { value: 1, done: false }
// throwing an exception
// 捕捉错误 { value: 1, done: false }
// 第三次运行next方法 { value: undefined, done: true }
// caller done

return()

return()方法:返回给定的值,并且终结遍历 Generator 函数

js
function* gen() {
  yield 1
  yield 2
  yield 3
}

var g = gen()

g.next() // { value: 1, done: false }
g.return('foo') // { value: "foo", done: true }
g.next() // { value: undefined, done: true }

不提供参数,则返回值的 value 属性为 undefined

Generator 函数内部有 try...finally 代码块,且正在执行 try 代码块,那么 return()方法会导致立刻进入 finally 代码块

js
function* numbers() {
  yield 1
  try {
    yield 2
    yield 3
  } finally {
    yield 4
    yield 5
  }
  yield 6
}
var g = numbers()
g.next() // { value: 1, done: false }
g.next() // { value: 2, done: false }
g.return(7) // { value: 4, done: false }
g.next() // { value: 5, done: false }
g.next() // { value: 7, done: true }

调用return()方法后,就开始执行finally代码块,不执行try里面剩下的代码了,然后等到finally代码块执行完,再返回return()方法指定的返回值

next、throw、return

next、throw、return 作用都是让 Generator 函数恢复执行,并且使用不同的语句替换 yield 表达式

next:将 yield 表达式替换成一个值

js
const g = function* (x, y) {
  let result = yield x + y
  return result
}

const gen = g(1, 2)
gen.next() // Object {value: 3, done: false}

gen.next(1) // Object {value: 1, done: true}
// 相当于将 let result = yield x + y
// 替换成 let result = 1;

throw:将 yield 表达式替换成一个 throw 语句

js
gen.throw(new Error('出错了')) // Uncaught Error: 出错了
// 相当于将 let result = yield x + y
// 替换成 let result = throw(new Error('出错了'));

return:将 yield 表达式替换成一个 return 语句

js
gen.return(2) // Object {value: 2, done: true}
// 相当于将 let result = yield x + y
// 替换成 let result = return 2;

yield * 表达式

在 Generator 函数内部,调用另一个 Generator 函数。需要在前者的函数体内部,自己手动完成遍历

js
function* foo() {
  yield 'a'
  yield 'b'
}

function* bar() {
  yield 'x'
  // 手动遍历 foo()
  for (let i of foo()) {
    console.log(i)
  }
  yield 'y'
}

for (let v of bar()) {
  console.log(v)
}
// x
// a
// b
// y

ES6 提供了 yield * 表达式,简化上述操作

js
function* foo() {
  yield 'a'
  yield 'b'
}

function* bar() {
  yield 'x'
  yield* foo()
  yield 'y'
}

for (let v of bar()) {
  console.log(v)
}
// x
// a
// b
// y

从语法角度看,如果 yield 表达式后面跟的是一个遍历器对象,需要在 yield 表达式后面加上星号,表明它返回的是一个遍历器对象

js
let delegatedIterator = (function* () {
  yield 'Hello!'
  yield 'Bye!'
})()

let delegatingIterator = (function* () {
  yield 'Greetings!'
  yield* delegatedIterator
  yield 'Ok, bye.'
})()

for (let value of delegatingIterator) {
  console.log(value)
}
// "Greetings!
// "Hello!"
// "Bye!"
// "Ok, bye."

yield 后面 Generator 函数中

  • 无 return 语句,for...of 简写形式
  • 有 return 语句,用var value = yield* iterator取值

任何数据结构只要有 Iterator 接口,就可以被yield*遍历

一个例子

js
function* genFuncWithReturn() {
  yield 'a'
  yield 'b'
  return 'The result'
}
function* logReturned(genObj) {
  let result = yield* genObj
  console.log(result)
}

;[...logReturned(genFuncWithReturn())]
// The result
// 值为 [ 'a', 'b' ]

存在两次遍历

  1. 扩展运算符遍历函数 logReturned 返回的遍历器对象
  2. yield*语句遍历函数 genFuncWithReturn 返回的遍历器对象

例子:取出嵌套数组的所有成员(打平数组)

js
function* iterTree(tree) {
  if (Array.isArray(tree)) {
    for (let i = 0; i < tree.length; i++) {
      yield* iterTree(tree[i])
    }
  } else {
    yield tree
  }
}

const tree = ['a', ['b', 'c'], ['d', 'e']]

for (let x of iterTree(tree)) {
  console.log(x)
}
// a
// b
// c
// d
// e

例子:yield* 语句遍历完全二叉树

js
// 下面是二叉树的构造函数,
// 三个参数分别是左树、当前节点和右树
function Tree(left, label, right) {
  this.left = left
  this.label = label
  this.right = right
}

// 下面是中序(inorder)遍历函数。
// 由于返回的是一个遍历器,所以要用generator函数。
// 函数体内采用递归算法,所以左树和右树要用yield*遍历
function* inorder(t) {
  if (t) {
    yield* inorder(t.left)
    yield t.label
    yield* inorder(t.right)
  }
}

// 下面生成二叉树
function make(array) {
  // 判断是否为叶节点
  if (array.length == 1) return new Tree(null, array[0], null)
  return new Tree(make(array[0]), array[1], make(array[2]))
}
let tree = make([[['a'], 'b', ['c']], 'd', [['e'], 'f', ['g']]])

// 遍历二叉树
var result = []
for (let node of inorder(tree)) {
  result.push(node)
}

result
// ['a', 'b', 'c', 'd', 'e', 'f', 'g']

作为对象属性的 Generator

js
let obj = {
  * myGeneratorMethod() {
    ···
  }
};
// 等效写法
let obj = {
  myGeneratorMethod: function* () {
    // ···
  }
};

Generator 中 this

Generator 函数总是返回一个遍历器。ES6 规定这个遍历器是 Generator 函数的实例,也继承了 Generator 函数的 prototype 对象上的方法

js
function* g() {}

g.prototype.hello = function () {
  return 'hi!'
}

let obj = g()

obj instanceof g // true
obj.hello() // 'hi!'

返回的是遍历器对象,而不是 this 对象

js
function* g() {
  this.a = 11
}

let obj = g()
obj.next()
obj.a // undefined

Generator 不能和 new 命令一起使用

js
function* F() {
  yield (this.x = 2)
  yield (this.y = 3)
}

new F()

功能:Generator 函数返回一个正常的对象实例,可以用 next 方法,又可以获得 this

实现:生成一个空对象,使用 call 方法绑定 Generator 函数内部的 this

js
function* F() {
  this.a = 1
  yield (this.b = 2)
  yield (this.c = 3)
}
var obj = {}
var f = F.call(obj)

f.next() // Object {value: 2, done: false}
f.next() // Object {value: 3, done: false}
f.next() // Object {value: undefined, done: true}

obj.a // 1
obj.b // 2
obj.c // 3

F.call 将 this 指向 obj,obj 可以等效于 F 的实例对象

问题:实例对象是 obj,遍历对象为 f,两者分离了。

解决:将 obj 换成 F.prototype

js
function* F() {
  this.a = 1
  yield (this.b = 2)
  yield (this.c = 3)
}
var f = F.call(F.prototype)

f.next() // Object {value: 2, done: false}
f.next() // Object {value: 3, done: false}
f.next() // Object {value: undefined, done: true}

f.a // 1
f.b // 2
f.c // 3

改造为构造函数,可以使用 new 命令

js
function* F() {
  this.a = 1
  yield (this.b = 2)
  yield (this.c = 3)
}

function Fun() {
  return F.call(F.prototype)
}

const f = new Fun()

f.next() // Object {value: 2, done: false}
f.next() // Object {value: 3, done: false}
f.next() // Object {value: undefined, done: true}

f.a // 1
f.b // 2
f.c // 3

含义

Generator 和 状态机

Generator 是实现状态机的最佳结构

js
var ticking = true
var clock = function () {
  if (ticking) console.log('Tick!')
  else console.log('Tock!')
  ticking = !ticking
}
js
var clock = function* () {
  while (true) {
    console.log('Tick!')
    yield
    console.log('Tock!')
    yield
  }
}

Generator 实现与 ES5 实现对比

  1. 更简洁,更安全(状态不会被非法篡改)
  2. 更符合函数式编程的思想

Generator 和 协程

协程(coroutine)是一种程序运行的方式,可以理解成“协作的线程”或“协作的函数”。

协程既可以用单线程实现,也可以用多线程实现。前者是一种特殊的子例程,后者是一种特殊的线程。

协程与子例程的差异

传统的“子例程”(subroutine)采用堆栈式“后进先出”的执行方式,只有当调用的子函数完全执行完毕,才会结束执行父函数

协程特点

并行执行:多个线程(单线程情况下,即多个函数)可以并行执行,但是只有一个线程(或函数)处于正在运行的状态,其他线程(或函数)都处于暂停态(suspended)

交换执行权:一个线程(或函数)执行到一半,可以暂停执行,将执行权交给另一个线程(或函数),等到稍后收回执行权的时候,再恢复执行。

这种可以并行执行、交换执行权的线程(或函数),就称为协程

从实现上看,在内存中,子例程只使用一个栈(stack),而协程是同时存在多个栈,但只有一个栈是在运行状态,也就是说,协程是以多占用内存为代价,实现多任务的并行。

协程作用

JavaScript 是单线程语言,只能保持一个调用栈。引入协程以后,每个任务可以保持自己的调用栈。最大好处,就是抛出错误的时候,可以找到原始的调用栈。不至于像异步操作的回调函数那样,一旦出错,原始的调用栈早就结束。

协程与普通线程的差异

与普通的线程很相似,都有自己的执行上下文、可以分享全局变量。 协程适合用于多任务运行的环境。

不同之处在于

协程:合作式。执行权由协程自己分配。 普通线程:抢先式。哪个线程优先得到资源,必须由运行环境决定

Generator 和 协程关系

Generator 函数是 ES6 对协程的实现,但属于不完全实现。Generator 函数被称为“半协程”(semi-coroutine),只有 Generator 函数的调用者,才能将程序的执行权还给 Generator 函数。如果是完全执行的协程,任何函数都可以让暂停的协程继续执行。

将 Generator 函数当作协程,完全可以将多个需要互相协作的任务写成 Generator 函数,使用 yield 表达式交换控制权

Generator 上下文

JavaScript 上下文:JavaScript 代码运行时,会产生一个全局的上下文环境(context,又称运行环境),包含了当前所有的变量和对象。执行函数(或块级代码),又会形成函数上下文,形成上下文堆栈。这个堆栈是“后进先出”的数据结构,最后产生的上下文环境首先执行完成,退出堆栈,然后再执行完成它下层的上下文,直至所有代码执行完成,堆栈清空。

Generator 上下文:Generator 函数不是这样,它执行产生的上下文环境,一旦遇到 yield 命令,就会暂时退出堆栈,但是并不消失,里面的所有变量和对象会冻结在当前状态。等到对它执行 next 命令时,这个上下文环境又会重新加入调用栈,冻结的变量和对象恢复执行

应用

Generator 可以暂停函数执行,返回任意表达式的值。

异步操作的同步化表达

Generator 函数的一个重要实际意义就是用来处理异步操作,改写回调函数

js
function* loadUI() {
  showLoadingScreen()
  yield loadUIDataAsynchronously()
  hideLoadingScreen()
}
var loader = loadUI()
// 加载UI
loader.next()

// 卸载UI
loader.next()

第一次调用 loadUI 函数时,该函数不会执行,仅返回一个遍历器。下一次对该遍历器调用 next 方法,则会显示 Loading 界面(showLoadingScreen),并且异步加载数据(loadUIDataAsynchronously)。等到数据加载完成,再一次使用 next 方法,则会隐藏 Loading 界面。

示例:Generator 函数逐行读取文本文件

js
function* numbers() {
  let file = new FileReader('numbers.txt')
  try {
    while (!file.eof) {
      yield parseInt(file.readLine(), 10)
    }
  } finally {
    file.close()
  }
}

控制流管理

场景:多步操作非常耗时,采用回调函数

js
step1(function (value1) {
  step2(value1, function (value2) {
    step3(value2, function (value3) {
      step4(value3, function (value4) {
        // Do something with value4
      })
    })
  })
})

采用 Promise 改写

js
Promise.resolve(step1)
  .then(step2)
  .then(step3)
  .then(step4)
  .then(
    function (value4) {
      // Do something with value4
    },
    function (error) {
      // Handle any error from step1 through step4
    }
  )
  .done()

Generator 函数改写

js
function* longRunningTask(value1) {
  try {
    var value2 = yield step1(value1)
    var value3 = yield step2(value2)
    var value4 = yield step3(value3)
    var value5 = yield step4(value4)
    // Do something with value4
  } catch (e) {
    // Handle any error from step1 through step4
  }
}

使用 一个 函数,按次序自动执行所有步骤

js
scheduler(longRunningTask(initialValue))

function scheduler(task) {
  var taskObj = task.next(task.value)
  // 如果Generator函数未结束,就继续调用
  if (!taskObj.done) {
    task.value = taskObj.value
    scheduler(task)
  }
}

只适合同步操作,因为得到返回值就向下运行,未考虑实际运行情况

利用 for...of 循环会自动依次执行 yield 命令的特性,提供一种更一般的控制流管理的方法

steps 将一个任务分为多个步骤

js
let steps = [step1Func, step2Func, step3Func]

function* iterateSteps(steps) {
  for (var i = 0; i < steps.length; i++) {
    var step = steps[i]
    yield step()
  }
}

jobs 将项目分解为多个任务

js
let jobs = [job1, job2, job3]

function* iterateJobs(jobs) {
  for (var i = 0; i < jobs.length; i++) {
    var job = jobs[i]
    yield* iterateSteps(job.steps)
  }
}

执行所有任务的所有步骤

js
for (var step of iterateJobs(jobs)) {
  console.log(step.id)
}

for...of 的本质是一个 while 循环

js
var it = iterateJobs(jobs)
var res = it.next()

while (!res.done) {
  var result = res.value
  // ...
  res = it.next()
}

部署 Iterator 接口

利用 Generator 函数,可以在任意对象上部署 Iterator 接口

js
function* iterEntries(obj) {
  let keys = Object.keys(obj)
  for (let i = 0; i < keys.length; i++) {
    let key = keys[i]
    yield [key, obj[key]]
  }
}

let myObj = { foo: 3, bar: 7 }

for (let [key, value] of iterEntries(myObj)) {
  console.log(key, value)
}

// foo 3
// bar 7

作为数据结构

Generator 函数可以返回一系列的值,这意味着它可以对任意表达式,提供类似数组的接口

js
function* doStuff() {
  yield fs.readFile.bind(null, 'hello.txt')
  yield fs.readFile.bind(null, 'world.txt')
  yield fs.readFile.bind(null, 'and-such.txt')
}
js
for (task of doStuff()) {
  // task是一个函数,可以像回调函数那样使用它
}

ES5 用数组模拟 Generator

js
function doStuff() {
  return [fs.readFile.bind(null, 'hello.txt'), fs.readFile.bind(null, 'world.txt'), fs.readFile.bind(null, 'and-such.txt')]
}

相关链接

[-] Generator 函数