JavaScript Generator 函数
Generator 函数是 ES6 提供的一种异步编程解决方案,语法行为与传统函数完全不同。此前,只在 dva(内部封装 redux-saga)里使用过,此次深入了解之。
Generator 函数
简介
基本概念
Generator 函数可以理解成一个状态机,封装了多个内部状态。执行 Generator 函数会返回一个遍历器对象,该遍历器对象可以依次遍历 Generator 函数内部的每一个状态。换言之,Generator 函数除了是状态机,还是一个遍历器对象生成函数。
Generator 函数的调用方法与普通函数一样。不同的是,调用 Generator 函数后,该函数并不执行,返回的也不是函数运行结果,而是一个指向内部状态的指针对象,也就是遍历器对象。
每次调用 next 方法,内部指针就从函数头部或上一次停下来的地方开始执行,直到遇到下一条 yield 语句(或 return 语句)为止。换言之,Generator 函数是分段执行的,yield 语句是暂停执行的标记,而 next 方法可以恢复执行。
function* helloWorldGenerator() { yield 'hello' yield 'world' return 'ending' } let hw = helloWorldGenerator() 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 方法的运行逻辑如下:
- 遇到 yield 语句就暂停执行后面的操作,并将紧跟在 yield 后的表达式的值作为返回对象的 value 属性值。
- 下一次调用 next 方法时再继续往下执行,直到遇到下一条 yield 语句。
- 如果没有再遇到新的 yield 语句,就一直运行到函数结束,直到 return 语句为止,并将 return 语句后面的表达式的值作为返回对象的 value 属性值。
- 如果该函数没有 return 语句,则返回对象的 value 属性值为 undefined。
只有调用 next 方法且内部指针指向该语句时才会执行 yield 语句后面的表达式,因此等于为 JavaScript 提供了手动的“惰性求值”(Lazy Evaluation)的语法功能。
下面的代码中,yield 后面的表达式不会立即求值,只会在 next 方法将指针移到这一句时才求值。
function* gen() { yield 123 + 456 }
Generator 函数可以不用 yield 语句,这时就变成了一个单纯的暂缓执行函数。下面的代码中,函数 f 如果是普通函数,在为变量 generator 赋值时就会执行。但是函数 f 是一个 Generator 函数,于是就变成只有调用 next 方法时才会执行。
function* f() { console.log('执行了!') } let generator = f() setTimeout(function() { generator.next() }, 2000)
yield 表达式如果用在另一个表达式之中,必须放在圆括号里面。
function* demo() { console.log('Hello' + yield 123) // SyntaxError console.log('Hello' + (yield 123)) // OK }
与 Iterator 接口的关系
任意一个对象的 Symbol.iterator 方法等于该对象的遍历器对象生成函数,调用该函数会返回该对象的一个遍历器对象。
由于 Generator 函数就是遍历器生成函数,因此可以把 Generator 赋值给对象的 Symbol.iterator 属性,从而使得该对象具有 Iterator 接口。
let myIterable = {} myIterable[Symbol.iterator] = function*() { yield 1 yield 2 yield 3 } ;[...myIterable] // [1, 2, 3]
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 开始递增。
Generator 函数从暂停状态到恢复运行,其上下文状态(context)是不变的。通过 next 方法的参数就有办法在 Generator 函数开始运行后继续向函数体内部注入值,从而调整函数行为。
function* foo(x) { let y = 2 * (yield x + 1) let z = yield y / 3 return x + y + z } let a = foo(5) a.next() // Object{value:6, done:false} a.next() // Object{value:NaN, done:false} a.next() // Object{value:NaN, done:true} let b = foo(5) b.next() // { value:6, done:false } b.next(12) // { value:8, done:false } b.next(13) // { value:42, done:true }
需要注意:由于 next 方法的参数表示上一条 yield 语句的返回值,所以第一次使用 next 方法时传递参数是无效的。V8 引擎直接忽略第一次使用 next 方法时的参数,只有从第二次使用 next 方法开始,参数才是有效的。从语义上讲,第一个 next 方法用来启动遍历器对象,所以不用带有参数。
function* dataConsumer() { console.log('Started') console.log(`1. ${yield}`) console.log(`2. ${yield}`) return 'result' } let genObj = dataConsumer() genObj.next() // Started genObj.next('a') // 1. a genObj.next('b') // 2. b
for...of 循环
for...of 循环可以自动遍历 Generator 函数生成的 Iterator 对象,且此时不再需要调用 next 方法。
function* foo() { yield 1 yield 2 return 3 } for (let v of foo()) { console.log(v) // 1 2 }
一旦 next 方法的返回对象的 done 属性为 true,for...of 循环就会终止,且不包含该返回对象,所以上面的 return 语句返回的 3 不包括在 for...of 循环中。
下面是一个利用 Generator 函数和 for...of 循环实现斐波那契数列的例子:
function* fibonacci() { let [prev, curr] = [0, 1] for (;;) { ;[prev, curr] = [curr, prev + curr] yield curr } } for (let n of fibonacci()) { if (n > 1000) break console.log(n) }
利用 for...of 循环,给对象加上遍历器接口:
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
另一种方法是直接将 Generator 函数添加到 Symbol.iterator 属性上:
function* objectEntries() { let propKeys = Object.keys(this) for (let propKey of propKeys) { yield [propKey, this[propKey]] } } jane[Symbol.iterator] = objectEntries
除了 for...of 循环,扩展运算符(...)、解构赋值和 Array.from 方法内部调用的都是遍历器接口。可以将 Generator 函数返回的 Iterator 对象作为参数:
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
Generator.prototype.throw()
Generator 函数返回的遍历器对象都有一个 throw 方法,可以在函数体外抛出错误,然后在 Generator 函数体内捕获。
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
上面的代码中,遍历器对象 i 连续抛出两个错误。第一个错误被 Generator 函数体内的 catch 语句捕获。第二个错误由于 Generator 函数内部的 catch 语句已经执行过了,不会再捕捉到这个错误,所以就被抛出了 Generator 函数体,被函数体外的 catch 语句捕获。
Generator.prototype.return()
Generator 函数返回的遍历器对象有一个 return 方法,可以返回给定的值,并终结 Generator 函数的遍历。
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 }
上面的代码中,遍历器对象 g 调用 return 方法后,返回值的 value 属性就是 return 方法的参数。同时,Generator 函数的遍历终止,返回值的 done 属性为 true。如果 return 方法调用时不提供参数,则返回值的 vaule 属性为 undefined。
如果 Generator 函数内部有 try...finally 代码块,那么 return 方法会推迟到 finally 代码块执行完再执行。
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 }
yield* 表达式
如果在 Generator 函数内部调用另一个 Generator 函数,需要用到 yield*
语句。
function* foo() { yield 'a' yield 'b' } function* bar() { yield 'x' yield* foo() yield 'y' }
yield*
后面的 Generator 函数(没有 return 语句时)不过是 for...of 的一种简写形式,完全可以用后者替代。反之,在有 return 语句时则需要用 var value = yield* iterator
的形式获取 return 语句的值。
任何数据结构只要有 Iterator 接口,就可以被 yield*
遍历。下面的代码中,yield 命令后面如果不加星号,返回的是整个数组,加了星号就表示返回的是数组的遍历器对象。
function* gen() { yield* ['a', 'b', 'c'] } gen().next() // { value:"a", done:false }
Generator 函数 this
Generator 函数总是返回一个遍历器,这个遍历器是 Generator 函数的实例,它继承了 Generator 函数的 prototype 对象上的方法。
function* g() {} g.prototype.hello = function() { return 'hi!' } let obj = g() obj instanceof g // true obj.hello() // 'hi!'
但是,如果把 g 当作普通的构造函数,则并不会生效,因为 g 返回的总是遍历器对象,而不是 this 对象。
function* g() { this.a = 11 } let obj = g() obj.a // undefined
Generator 函数也不能跟 new 命令一起用,否则会报错。
有一个变通方法可以让 Generator 函数返回一个正常的对象实例,既可以用 next 方法,又可以获得正常的 this:
function* gen() { this.a = 1 yield (this.b = 2) yield (this.c = 3) } function F() { return gen.call(gen.prototype) } var f = new F() 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
上面代码中,使用 call 方法绑定 Generator 函数内部的 this 到原型 prototype 上,调用 next 方法完成 gen 内部所有代码的运行。这时,所有内部属性都绑定在原型对象上了,因此原型对象也就成了 gen 的实例。
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论