ES6 新特性之 Generator 函数的语法

基本概念

Generator 函数是一个状态机,封装了多个内部状态,执行 Generator 函数会返回一个遍历器对象,可以依次遍历 Generator 函数内部的每一个状态

Generator 的两个特征:

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

上面代码定义了一个 Generator 函数 helloWorldGenerator,它内部有两个 yield 表达式(hello 和 world),即该函数有三个状态:hello,world 和 return 语句(结束执行)

Generator 函数与普通函数的不同之处在于,调用 Generator 函数后,该函数并不执行,返回的也不是函数运行结果,而是一个指向内部状态的指针对象,即遍历器对象(Iterator Object)

yield 表达式

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

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

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

hw.next()
// { value: 'hello', done: false }
hw.next()
// { value: 'world', done: false }
hw.next()
// { value: 'ending', done: true }
hw.next()
// { value: undefined, done: true }

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

function* gen () {
  yield  123 + 456
}

上面代码中,yield 后面的表达式 123 + 456 不会立即求值,只会在 next 方法将指针移到这一句时,才会求值

Generator 函数中如果没有 yield 表达式,就变成了一个单纯的暂缓执行函数

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

let generator = f()
setTimeout(function () {
  generator.next()
}, 2000)
// 2s 后打印“执行了!”

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

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

next 方法的参数

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

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

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

上面代码先定义了一个可以无限运行的 Generator 函数 f,如果 next 方法没有参数,每次运行到 yield 表达式,变量 reset 的值总是 undefined
当 next 方法带一个参数 true 时,变量 reset 就被重置为 true,因此 i 会等于 -1,下一轮循环就会从 -1 开始递增

与 Iterator 接口的关系

由于 Generator 函数就是遍历器生成函数,因此可以把 Generator 赋值给对象的 Symbol.iterator 属性,从而使得该对象具有 Iterator 接口

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

Generator 函数执行后,返回一个遍历器对象,该对象本身也具有 Symbol.iterator 属性,执行后返回自身,所以可以直接用 for...of 遍历

function* foo() {
  yield 1
  yield 2
  yield 3
  yield 4
  yield 5
  return 6
}
for (let v of foo()) {
  console.log(v)
}
// 1 2 3 4 5

上面代码使用 for...of 循环,依次显示 5 个 yield 表达式的值
这里需要注意,一旦 next 方法的返回对象的 done 属性为 true,for...of 循环就会中止,且不包含该返回对象,所以上面代码的 return 语句返回的 6,不包括在 for...of 循环之中

Generator.prototype.throw

Generator 函数返回的遍历器对象,都有一个 throw 方法,可以在函数体外抛出错误,然后在 Generator 函数体内捕获

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

let i = g()
i.next()

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

上面代码中,遍历器对象 i 连续抛出两个错误:
第一个错误被 Generator 函数体内的 catch 语句捕获;
第二次抛出错误,由于 Generator 函数内部的 catch 语句已经执行过了,不会再捕捉到这个错误,所以这个错误就被抛出了 Generator 函数体,被函数体外的 catch 语句捕获

注意:不要混淆遍历器对象的 throw 方法和全局的 throw 命令,后者只能被函数体外的catch语句捕获
let g = function* () {
  while (true) {
    try {
      yield
    } catch (e) {
      if (e != 'a') throw e
      console.log('内部捕获', e)
    }
  }
}

let i = g()
i.next()

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

上面代码因为函数体外的 catch 语句块捕获了抛出的 a 错误,所以不会再继续执行 try 代码块里面剩余的语句

let g = function* () {
  while (true) {
    yield
    console.log('内部捕获', e)
  }
}

let i = g()
i.next()

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

上面代码中,Generator 函数 g 内部没有部署 try...catch 代码块,所以抛出的错误直接被外部 catch 代码块捕获

throw 方法抛出的错误要被内部捕获,前提是必须至少执行过一次 next 方法

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

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

上面代码中,g.throw(1) 执行时,next 方法一次都没有执行过,这时抛出的错误不会被内部捕获,而是直接在外部抛出,导致程序出错

throw 方法被捕获以后,会附带执行下一条 yield 表达式,也就是说,会附带执行一次 next 方法

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

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

上面代码中,g.throw 方法被捕获以后,自动执行了一次 next 方法,所以会打印 b

Generator.prototype.return

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

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

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

上面代码中,遍历器对象 g 调用 return 方法后,返回值的 value 属性就是 return 方法的参数 foo,并且,Generator 函数的遍历就终止,返回值的 done 属性为 true,以后再调用 next 方法,done 属性总是返回 true

如果return方法调用时,不提供参数,则返回值的value属性为 undefined

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

let g = gen()
g.next()        // { value: 1, done: false }
g.return() // { value: undefined, done: true }

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

function* numbers () {
  yield 1
  try {
    yield 2
    yield 3
  } finally {
    yield 4
    yield 5
  }
  yield 6
}
let 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 代码块,然后等到 finally 代码块执行完,再执行 return 方法

yield* 表达式

ES6 提供了 yield* 表达式,作为解决办法,用来在一个 Generator 函数里面执行另一个 Generator 函数

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

// 等同于
function* bar() {
  yield 'x'
  yield 'a'
  yield 'b'
  yield 'y'
}

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

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

再来看一个对比的例子

function* inner() {
  yield 'hello!'
}

function* outer1() {
  yield 'open'
  yield inner()
  yield 'close'
}

let gen = outer1()
gen.next().value // "open"
gen.next().value // 返回一个遍历器对象
gen.next().value // "close"

function* outer2() {
  yield 'open'
  yield* inner()
  yield 'close'
}

let gen = outer2()
gen.next().value // "open"
gen.next().value // "hello!"
gen.next().value // "close"

上面例子中,outer2 使用了yield*,outer1 没使用,结果就是,outer1 返回一个遍历器对象,outer2 返回该遍历器对象的内部值

本文转载自 http://es6.ruanyifeng.com/#docs/generator 并进行整理

除特殊说明外本人博客均属原创,转载请注明出处:http://blog.johnhan.cn/blog_1047.html
鄂ICP备17018604号-1  鄂公网安备42060702000030号