今天我们要来学习生成器的有关内容。生成器是ES6的新特性。生成器的定义方式是先创建一个函数,调用这个函数再返回生成器对象g。这个生成器对象g是一个可迭代对象,我们可以通过Array.from(g),[…g],for..of循环来使用它。
1.生成器函数的定义
生成器形式是一个函数,function关键字的后面、函数名称的前面加一个星号来表示它是一个生成器,只要是可以定义函数的地方就可以定义生成器。
注意:生成器函数的星号不受两侧的空格影响。这个星号可以紧接着function关键字,可以处在函数名和function关键字之间并用若干空格和两者隔开,也可以在其后紧接着函数名。例如:
function * newName(){}
function* newName(){}
function *newName(){}
如上代码中这三种形式都是等价的。
生成器函数允许我们声明一种特殊的迭代器,这种迭代器会推迟代码的执行,同时保持自己的上下文。下面我来看一下生成器函数的迭代:
2.生成器函数的迭代
function * newName(){
yield 'new'
yield 'name'
yield 'study'
yield 'JavaScript'
}
const test = newName()
let res1 = typeof test[Symbol.iterator] === 'function'
console.log('res1',res1)
let res2 = typeof test.next === 'function'
console.log('res2',res2)
let res3 = test[Symbol.iterator]() === test
console.log('res3',res3)
console.log(Array.from(test))
之前我们学习过迭代器,每次迭代都会调用next方法从序列中取出一个值。但是在生成器中看不到返回值的next方法,只能看到向序列中添加的yield关键字。
生成器对象同时遵守可迭代协议和迭代器协议,通过如上代码我们可以知道:
(1)生成器对象test是通过生成器函数newName创建的;
(2)生成器对象test是一个可迭代对象,因为它拥有Symbol.iterator属性,此属性是一个方法;
(3)生成器对象test也是一个迭代器,因为它有一个next方法;
(4)生成器对象test的迭代器就是它自己。
以上代码运行结果如下所示:
迭代会触发生成器函数中的副作用。当生成器函数恢复执行以返回序列中下一个元素的时候,每一个yield语句后面的console.log()语句都会执行:
function * newName(){
yield 'new'
console.log(1)
yield 'name'
console.log(2)
yield 'study'
console.log(3)
yield 'JavaScript'
console.log(4)
}
console.log([...newName()])
运行效果如下图:
再来看一个使用for...of的例子:
function * newName(){
yield 'new'
console.log(1)
yield 'name'
console.log(2)
yield 'study'
console.log(3)
yield 'JavaScript'
console.log(4)
}
for(let item of newName()){
console.log(item)
}
运行结果如下图:
3.使用yield* 委托生成序列
生成器函数可以使用 yield* 将生成序列的任务委托给一个生成器对象或其他可迭代对象。如下代码所示:
function * test() {
yield* 'hello'
}
console.log([...test()])
运行结果如下图:
当然直接使用[…'hello']更简单。然而,有多条yield语句时,委托的作用就体现出来了,如下代码所示:
function * test(name) {
yield* 'hello '
yield* name
}
console.log([...test('newName')])
运行结果如下图:
我们可以通过yield* 将生成序列的任务委托给任何遵守可迭代协议的对象,而不仅仅是字符串。如下代码就展示了如何使用yield和yield*,并组合其他生成器函数、可迭代对象和扩展操作符来描述一个值序列:
const test1 = {
[Symbol.iterator]() {
const items = ['n','e','w']
return {
next: () => ({
done: items.length === 0,
value: items.shift()
})
}
}
}
function* test2(num1,num2) {
yield num1+num2
yield num2*num2
}
function* test3(){
yield* test1
yield 777
yield* ['new','name']
yield* [...test2(2,3)]
yield [...test2(2,3)]
}
console.log([...test3()])
运行结果如下图:
除了使用Array.from(g),[…g],for..of的方式来迭代生成器对象,我们也可以直接手工迭代生成器对象:
4.手工迭代生成器
能够手工迭代生成器的原因是,生成器器对象与其他可迭代对象一样,有Symbol.iterator属性,也就可以通过next方法按需取值。
如下代码展示了:创建了生成器newName,以及如何使用生成器对象generatorTest和while循环来手工迭代它:
function * newName(){
yield 'new'
yield 'name'
yield 'study'
yield 'JavaScript'
}
const generatorTest = newName()
while(true) {
const item = generatorTest.next()
if(item.done) {
break
}
console.log(item.value)
}
代码执行结果如下图所示
和for…of循环比较而言,用迭代器遍历生成器看起来比较麻烦,但是有一些场景却比较合适。因为for…of是一个同步循环,而有迭代器的话,什么时候调用next()方法就可以由我们控制。
迭代完生成器generatorTest的整个序列后,再调用next()方法不会有什么变化,只会返回{done:true},如下代码所示:
function * newName(){
yield 'new'
yield 'name'
yield 'study'
yield 'JavaScript'
}
const generatorTest = newName()
while(true) {
const item = generatorTest.next()
if(item.done) {
break
}
console.log(item.value)
}
console.log(generatorTest.next())
console.log(generatorTest.next())
代码执行结果如下图所示:
如下代码定义了一个生成无穷斐波那契序列的生成器,然后我们实例化了生成器对象并读取序列中的前3个值:
function *f() {
let f1 = 0
let f2 = 1
while(true) {
yield f2
const next = f1 + f2
f1 = f2
f2 = next
}
}
const g = f()
console.log(g.next())
console.log(g.next())
console.log(g.next())
代码运行结果如下图所示:
也可以使用可迭代对象来实现与此相似的效果:
const f = {
[Symbol.iterator]() {
let f1 = 0
let f2 = 1
return{
next() {
const value = f2
const next = f1 + f2
f1 = f2
f2 = next
return {
value,
done: false
}
}
}
}
}
const g = f[Symbol.iterator]()
console.log(g.next())
console.log(g.next())
console.log(g.next())
console.log(g.next())
代码运行结果如下图所示:
如果将Symbol.iterator属性改写成为一个生成器函数,那么它照样会工作,如下代码所示:
const f = {
*[Symbol.iterator]() {
let f1 = 0
let f2 = 1
while(true) {
yield f2
const next = f1 + f2
f1 = f2
f2 = next
}
}
}
const g = f[Symbol.iterator]()
console.log(g.next())
console.log(g.next())
console.log(g.next())
console.log(g.next())
console.log(g.next())
代码运行结果如下图所示:
验证可迭代协议的有效性:如下代码中我们使用了for…of 验证了如果symbol.iterator属性是一个生成器的情况。
const f = {
*[Symbol.iterator]() {
let f1 = 0
let f2 = 1
while(true) {
yield f2
const next = f1 + f2
f1 = f2
f2 = next
}
}
}
for (let item of f) {
if(item >10) {
break
}
console.log(item)
}
运行结果如下图所示:
5.给生成器传参数以及生成器的关闭
可以使用yield实现输入和输出,上一次让生成器函数暂停的yield关键字会接收到传给next方法的第一个值。
需要注意的是第一次调用next方法传入的值不会被使用,因为这一次调用是为了开始执行生成器函数:
function * newName(item){
console.log(item)
console.log(yield)
console.log(yield)
}
let g = newName('new')
g.next('name')
g.next('study')
g.next('JavaScript')
g.next('good')
运行结果如下图所示:
再来看一段代码:
function * newName(){
return yield 'new'
}
let g = newName()
console.log(g.next())
console.log(g.next('name'))
其运行结果为:
因为必须要对整个表达式求值才能确定要返回的值,所以它在遇到yield关键字的时候暂停执行并计算出要返回的值:“new”。下一次调用next传入“name”,作为交给同一个yield的值。然后这个值被确定为本次生成器函数要返回的值。
yield关键值并非只能使用一次,如下代码就多次使用yield定义了一个无穷计数的生成器函数:
// 使用生成器定义一个无穷计数生成器函数
function * generateCount(){
for(let i=0;;i++){
yield i
}
}
let couterGen = generateCount();
console.log(couterGen.next().value)
console.log(couterGen.next().value)
console.log(couterGen.next().value)
console.log(couterGen.next().value)
console.log(couterGen.next().value)
console.log(couterGen.next().value)
运行结果如下图所示:
使用生成器可以实现范围和填充数组:
function* range(start,end) {
while(end>start){
yield start++
}
}
for(const x of range(4,7)) {
console.log(x)
}
如上代码实现了范围,代码运行效果如下图所示:
如下代码实现了填充数组:
function* zeroes(n) {
while(n--) {
yield 0;
}
}
console.log(Array.from(zeroes(8)))
运行结果如下图所示:
生成器可选的return方法用于提前终止迭代器,还有throw方法也可以强制生成器进入关闭状态。
如下代码调用了生成器的return方法:
function * newName(){
yield 'new'
yield 'name'
yield 'study'
yield 'JavaScript'
}
const g = newName()
console.log(g.next())
console.log(g.next())
console.log(g.return())
console.log(g.next())
运行结果如下图所示:
使用try/finally块可以避免立即终止迭代序列,因为finally块内的代码会在执行流退出函数前执行,这意味着finally块中的yield表达式会继续回送序列中值。
function * newName(){
try{
yield 'new'
} finally {
yield 'name'
yield 'study'
}
yield 'JavaScript'
}
const g = newName()
console.log(g.next())
console.log(g.return())
console.log(g.next())
运行结果如下图所示:
如果使用扩展操作符、Array.from或者for…of来迭代这个生成器,那么无论return语句放在什么位置,结果中都不会包含返回的value,如下代码所示:
function * newName(){
yield 'new'
yield 'name'
return 'study'
yield 'JavaScript'
}
for(const item of newName()) {
console.log(item)
}
console.log([...newName()])
console.log(Array.from(newName()))
运行结果如下图所示:
要取生成器返回的值则必须使用next()方法去获取:
function * newName(){
yield 'new'
yield 'name'
return 'study'
yield 'JavaScript'
}
const g = newName()
console.log(g.next())
console.log(g.next())
console.log(g.next())
console.log(g.next())
运行结果如下图所示:
6.生成器的应用
如下代码中定义了树形结构,并定义了一个基于生成器函数的深度优先遍历方法:
// 生成器遍历树形结构
class Node {
constructor(value,...children) {
this.value = value
this.children = children
}
}
const root = new Node(
1,
new Node(2),
new Node(3,
new Node(4,
new Node(5,
new Node(6)
),
new Node(7)
)
),
new Node(8,
new Node(9),
new Node(10)
)
)
function* depthFirst(node) {
yield node.value
for(const child of node.children) {
yield* depthFirst(child)
}
}
console.log([...depthFirst(root)])
在深度优先遍历方法中,返回当前节点的值,然后再迭代其子节点,使用yield*操作符来拼接迭代器递归的结果,返回序列中的每一项。
也可以将上面定义的depthFirst生成器作为迭代器,将node转换为可迭代对象,如下代码所示:
// 生成器遍历树形结构
class Node {
constructor(value,...children) {
this.value = value
this.children = children
}
*[Symbol.iterator](){
yield this.value
for(const child of this.children) {
yield* child
}
}
}
const root = new Node(
1,
new Node(2),
new Node(3,
new Node(4,
new Node(5,
new Node(6)
),
new Node(7)
)
),
new Node(8,
new Node(9),
new Node(10)
)
)
console.log([...root])
运行结果如下:
如果要使用宽度优先遍历来遍历树的节点,则可以使用队列来保存尚未访问的节点,在遍历的每一步都是先打印当前节点值,并将当前节点的所有子节点放入队列中。
// 生成器遍历树形结构
class Node {
constructor(value,...children) {
this.value = value
this.children = children
}
*[Symbol.iterator](){
const queue = [this]
while(queue.length) {
const node = queue.shift()
yield node.value
queue.push(...node.children)
}
}
}
const root = new Node(
1,
new Node(2),
new Node(3,
new Node(4,
new Node(5,
new Node(6)
),
new Node(7)
)
),
new Node(8,
new Node(9),
new Node(10)
)
)
console.log([...root])
运行结果为:
也可以使用生成器定义一个计数器,代码如下所示:
// 使用生成器定义一个计数器
function* times(n){
while(n--) {
yield
}
}
for(let item of times(3)) {
console.log('new name')
}
//输出3次 new name
今天我们要一起学习的内容就这些,下次我们将一起学习Promise。来一张图总结回顾今日的内容: