跳转到内容

Generator函数的语法

简介

基本概念

Generator 函数是 ES6 提供的一种异步编程解决方案,从语法角度,他是一个状态机,封装了多个内部状态。执行后会返回一个遍历器对象,可以依次遍历 Generator 函数内部的每一个状态。也就是说他还是一个遍历器对象生成函数。

形式上,他是一个普通函数,但是有两个特征。一是,function 关键字与函数名之间有一个星号;二是,函数体内部使用 yield 表达式,定义不同的内部状态。

javascript
function* helloWorldGenerator() {
  yield 'hello';
  yield 'world';
  return 'ending';
}

var hw = helloWorldGenerator();

调用 Generator 函数后,该函数并不执行,返回的也不是函数运行结果,而是一个指向内部状态的指针对象,也就是遍历器对象。必须调用遍历器对象的 next 方法,使得指针指向下一个状态。

javascript
hw.next()
// { value: 'hello', done: false }

hw.next()
// { value: 'world', done: false }

hw.next()
// { value: 'ending', done: true }

hw.next()
// { value: undefined, done: true }

上面代码中,Generator 函数 helloWorldGenerator 有三个状态:helloworldreturn 语句(即函数结束)。调用 next 方法,指针会从函数头部或上一次停下来的地方开始执行,直到遇到下一个 yield 表达式或 return 语句为止。换言之,Generator 函数是分段执行的,yield 表达式是暂停执行的标记,而 next 方法可以恢复执行。直到遇到 return 语句或函数结束,才会结束执行。后续再调用 return,也只会返回 { value: undefined, done: true }

ES6没有规定 * 号写在哪,以下几种方式都能通过:

  • function* foo() {}
  • function *foo() {}
  • function*foo() {}
  • function * foo() {}

yield表达式

由于 Generator 函数返回的遍历器对象,只有调用 next 方法才会遍历下一个内部状态,所以其实提供了一种可以暂停执行的函数。yield 表达式就是暂停标志。

遍历器对象的 next 方法的运行逻辑如下:

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

⚠ 注意

yield 表达式后面的表达式,只有当调用 next 方法,内部指针指向该语句时才会执行,因此等于为 JavaScript 提供了手动的“惰性求值”(Lazy Evaluation)的语法功能。

js
fucntion* add() {
  yield 1 + 2 * 3
}

yield 后面的表达式不会立即计算,只有 next 方法指向针移到这句时才会求值

yieldreturn 既相似又不同。相似在于都能返回后面的值作 value。不同在于遇到 yield 会暂停执行,下一次会从这里继续执行,一个函数可以执行多次 yieldreturn 不具备记忆能力,一个函数只能执行一次 return

Generator 函数可以不加 yield,这样他就只是一个单纯的暂缓执行函数。

js
function* fn() {
  console.log('run')
}

var f = fn()

setTimeout(() => {
  f.next()
}, 1000)

如果是普通函数,在为变量 f 赋值时就会立即执行,Generator 函数则会等到调用 f.next() 时才执行。

  1. yield 表达式只能用在 Generator 函数里面,它的外层必须是 Generator 函数,用在其他地方都会报错。

    js
    function* fn(arr) {
       arr.forEach(item = {
         yield item * 2 // 报错
       })
    }

    上方代码 yield 外层是 forEach 的回调函数,因此报错,解决方法是把 forEach 改成 for,保证 yield 外层是 Generator 函数。

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

    js
    function* fn() {
       console.log('Hello' + yield) // 报错
       console.log('Hello' + yield 123) // 不报错
    
       console.log('Hello' + (yield)) // 不报错
       console.log('Hello' + (yield 123)) // 不报错
    }

与Iterator接口的关系

前面有提到过,任意一个对象的 Symbol.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 属性,从而使得 myIterable 对象具有了 Iterator 接口,可以被 ... 运算符遍历了。

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

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

var f = fn()

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

next方法的参数

yield 表达式本身没有返回值,或者说总是返回 undefinednext 方法可以带一个参数,该参数会被当作上一个 yield 表达式的返回值。

js
function* fn() {
  var f1 = yield 1
  var f2 = yield 2
  var f3 = yield 3
  console.log(f1, f2, f3)
}

var f = fn()

f.next() // { value: 1, done: false }
f.next('a') // { value: 2, done: false } f1 = 'a'
f.next('b') // { value: 3, done: false } f2 = 'b'
f.next('c') // { value: undefined, done: true } f3 = 'c'

这使得 Generator 函数可以通过 next 方法的参数在开始运行后继续向函数内部注入值,从而调整函数行为。

⚠ 注意

由于 next 方法的参数表示上一条 yield 语句的返回值,所以第一次使用 next 方法时,传递参数是无效的。V8 引擎直接忽略第一次使用 next 方法时的参数,只有从第二次使用 next 方法开始,参数才是有效的。从语义上讲,第一个 next 方法用来启动遍历器对象,所以不用带有参数。

for...of循环

for...of 循环可以自动遍历 Generator 函数运行时生成的 Iterator 对象,且此时不再需要调用 next 方法。

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

for (var f of fn()) {
  console.log(f)
}

上面代码使用 for...of 循环,依次显示 1、2、3。

⚠ 注意

for...of 会在 next 方法返回对象的 donetrue 时停止循环,因此 return 语句的返回值不会遍历到。

JavaScript 原生对象没有遍历接口,可以通过 Generator 函数为它加上遍历器接口或者给对象的 Symbol.iterator 属性绑定 Generator 函数,这样就能使用 for...of 循环遍历对象了。

js
/**
 * 添加遍历器接口
 */
function* objectEntries(obj) {
  let propKeys = Object.keys(obj)

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

let obj = { first: 'hello', last: 'world' }

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

/**
 * Symbol.iterator 属性绑定 Generator 函数
 */
obj[Symbol.iterator] = function* () {
  let propKeys = Object.keys(this)
  for (let propKey of propKeys) {
    yield [propKey, this[propKey]]
  }
}

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

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

Generator.prototype.throw()

Generator 函数返回的遍历器对象都有一个 throw 方法,用于抛出错误,然后在 Generator 函数体内部 用 try...catch 捕获。throw 方法可以接受一个参数,该参数会被 catch 语句接收,建议抛出 Error 对象。

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

上面代码示例可以看出,第一次 throw 抛出错误被 Generator 函数内部的 try...catch 捕获。第二次 throw 抛出错误,由于内部的 catch 语句已经执行过了,无法再捕捉这个错误了,因此被抛出 Generator 函数体,在外部 try...catch 捕获。

如果内部没有 try...catch 语句,那么这个错误将直接抛到 Generator 函数体外部。此时函数会因为遍历报错导致直接终止遍历。

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

var g = fn()

g.next() // 1
g.throw('a') // Uncaught a 循环终止

⚠ 注意

不要混淆 throw 方法与函数体内部的 throw 命令语句,后者只能被函数体内部的 catch 语句捕获。

throw 方法在被 捕获 后会附带执行下一条的 yield 语句,即附带执行一次 next 方法。

这种函数体内捕获错误的机制大大方便了对错误的处理。对于多个 yield 表达式,可以 只用 try...catch 代码块来捕获错误。如果使用回调函数的写法想要捕获多个错误, 就不得不每个函数写←个错误处理语句,而现在只在 Generator 函数内部写 catch 语句 就可以了。

Generator.prototype.return()

Generator 函数返回的遍历器对象,还有一个 return 方法,可以返回给定的值,并且终结遍历 Generator 函数。

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

var g = fn()

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

如果 Generator 函数内部有 try...finally 代码块,那么 return 方法会推迟到 finally 代码块执行完再执行。

js
function* fn() {
  try {
    yield 1
  }
  finally {
    yield 2
    yield 3
  }
}

var g = fn()

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

yield*表达式

如果 Generator 函数内部,调用另一个 Generator 函数,默认情况下是没有效果的。

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

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

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

上面代码中,foo() 仅仅是一个单纯的函数调用,不会执行。但是,如果使用 yield* 表达式,foo() 就会作为 Generator 函数执行。

js
function* bar() {
  yield 'x'
  yield* foo()
  /**
   * 等同于:
   * for (let v of foo()) {
   *     yield v
   * }
   */
  yield 'y'
}

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

从语法角度看,如果 yield 命令后面跟的是一个遍历器对象,那么需要在 yield 命令后面加 * 星号,表明它返回的是一个遍历器。这样,yield* 表达式就会调用遍历器接口,也就是调用 Generator 函数。这被称为 yield* 语句。

yield* 表达式可以很方便地遍历所有有 Iterator 接口的数据。。

js
function* fn() {
  yield 'hello'
  yield* 'hello'
  yield* [1, 2, 3]
}

// 'hello'
// 'h'
// 'e'
// 'l'
// 'l'
// 'o'
// 1
// 2
// 3

如果代理的 Generator 函数有 return 语句,那么 return 语句的返回值,就作为 yield* 表达式的返回值。

js
function* fn() {
  yield 2
  yield 3
  return 'foo'
}

function* bar() {
  yield 1
  var v = yield* fn()
  console.log('v: ' + v)
  yield 4
}

let b = bar()

b.next() // { value: 1, done: false }
b.next() // { value: 2, done: false }
b.next() // { value: 3, done: false }
b.next() // v: foo { value: 4, done: false }
b.next() // { value: undefined, done: true }

作为对象属性的Generator函数

如果对象的方法是 Generator 函数,可以将该对象的方法名放在方括号内。

js
let obj = {
  * myGeneratorMethod() {
    // ···
  }
}

/**
 * 等价于以下写法
 */
let obj = {
  myGeneratorMethod: function* () {
    // ···
  }
}

Generator函数的this

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

js
function* g() {}

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

let obj = g()

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

上面代码表明,Generator 函数 g 返回的遍历器 obj,是 g 的实例,继承了 g.prototype。但是,如果把 Generator 函数当作普通的构造函数,并不会生效,因为 Generator 函数返回的总是遍历器对象,而不是 this 对象。

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

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

上面代码中,Generator 函数 gthis 对象上添加了一个属性 a,但是 obj 对象拿不到这个属性。

含义

总结

Generator 函数是 ES6 引入的一种异步编程解决方案,它允许函数在执行过程中被暂停和恢复。这种函数在形式上是普通函数,但有两个特征:一是在 function 关键字和函数名之间有一个星号(*),二是函数体内部使用 yield 表达式来定义不同的内部状态。

Generator 函数执行后返回一个遍历器对象,可以通过调用该对象的 next 方法来遍历函数内部的每一个状态。yield表达式用于暂停函数执行,直到下一次调用 next 方法时才继续执行。此外,yield 表达式后面的表达式只有在调用 next 方法,内部指针指向该语句时才会执行,这为 JavaScript 提供了手动的 “惰性求值” 功能。

Generator 函数与 Iterator 接口紧密相关,可以将 Generator 函数赋值给对象的Symbol.iterator属性,使其具有 Iterator 接口。这样,对象就可以被 for...of 循环遍历,或者使用扩展运算符(...)和 Array.from 方法。

next 方法可以带一个参数,该参数会被当作上一个 yield 表达式的返回值,这使得 Generator 函数可以在开始运行后继续向函数内部注入值,从而调整函数行为。for...of 循环可以自动遍历 Generator 函数运行时生成的 Iterator 对象,且此时不再需要调用 next 方法。

Generator 函数还提供了 throwreturn 方法。throw 方法用于抛出错误,并在 Generator 函数体内部用 try...catch 捕获。return 方法可以返回给定的值,并终结遍历 Generator 函数。如果 Generator 函数内部有 try...finally 代码块,那么 return 方法会推迟到 finally 代码块执行完再执行。

yield* 表达式允许在一个 Generator 函数内部调用另一个 Generator 函数,或者遍历一个具有 Iterator 接口的数据结构。这使得代码更加简洁,可以方便地遍历所有有 Iterator 接口的数据。