Skip to content

读者可能会发现,迭代器用起来其实是很方便的东西,但是写起来可就麻烦了,需要给对象添加一个 Symbol.iterator 属性,然后里面返回一个迭代器,迭代器里要自己写 next 方法。生成器最初就是用来帮助我们书写迭代器的。

但是由于生成器可以玩出各种奇奇怪怪的操作,所以后续又增强了生成器的功能,因此生成器和迭代器本身有很多关联,并且有自己特别的地方。

所谓的生成器,实际上是通过 Generator 类创建的对象,但是我们无法使用 new 关键字手动创建。生成器有一个很特别的地方,既是一个迭代器,又是一个可迭代对象。什么意思呢,也就是说生成器里面有一个 next 方法,同时又有 Symbol.iterator 这个方法。

一个生成器必须使用生成器函数来生成,我们只要在函数名前面加上一个 *,该函数就变成了一个生成器函数,该函数一定返回一个生成器,这里做一个区分,在 C 语言中,在函数名前面加上 *,表示该函数返回的是一个指针。

js
function *method(){//这就是生成器函数

}

function* method2(){//这也是生成器函数

}

注意,async 函数返回一个 Promise,而生成器函数返回一个生成器,这二者是矛盾的,所以不存在异步的生成器函数。

当加上 * 之后,这个函数的返回值就是一个生成器,也就是一个可迭代的迭代器,既有 next 方法,又有 Symbol.iterator 方法。

生成器函数被调用后,函数体不会执行,生成器函数的函数体是为了每次迭代提供数据的。每一次调用 next 方法,将导致生成器运行到下一个 yield 关键字为止。

yield 是一关键字,该关键字必须在生成器函数内部使用,用于产生一个迭代数据。

我们可以看下面的例子

js
function *test(){
	console.log("第一次运行");
	yield 1;
	console.log("第二次运行");
	yield 2;
	console.log("第三次运行");
	yield 3;
	console.log("第四次运行");
}

const generator = test();//这个函数的函数体不会执行,会返回一个生成器

generator.next();//该表达式会执行console.log("第一次运行");并返回{value:1,done:false}

generator.next();//该表达式会执行console.log("第二次运行");并返回{value:2,done:false}

generator.next();//该表达式会执行console.log("第三次运行");并返回{value:3,done:false}

generator.next();//该表达式会执行console.log("第四次运行");并返回{value:undefined,done:true}

可以看到,生成器函数调用后返回一个生成器,生成器本质上是一个迭代器,所以可以调用 next 方法,每次调用 next 方法就会运行生成器函数内部的一部分,调一个方法就会运行到下一个 yield 关键字。

从这里我们应该有这样一种感觉,生成器函数被分段执行了。

我们可以使用生成器很方便地写出一个数组的迭代器函数。

js
// 创建一个包含数字1到5的数组
const arr = [1, 2, 3, 4, 5];
/**
 * 生成器函数,用于遍历给定数组并逐个返回数组的元素。
 * @param {Array} arr - 需要遍历的数组。
 * @yields {number} - 遍历数组时每次返回的当前元素。
 */
function* generator(arr) {
	// 使用for循环遍历数组,通过yield关键字逐个返回元素
	for (let i = 0; i < arr.length; i++) {
		yield arr[i];
	}
}
// 创建一个基于生成器函数的迭代器
const iterator = generator(arr);
/**
 * 使用for...of循环遍历生成器返回的迭代器,输出每个迭代步骤的值。
 * 这种方式允许我们逐个处理数组元素,而无需一次性加载整个数组。
 */
for (let data = iterator.next(); !data.done; data = iterator.next()) {
	console.log(data.value); // 输出当前迭代步骤中的值
}

生成器函数可以很方便地书写迭代器生成函数,实际上,生成器就像断点一样,调用一次 next 就会运行到下一个 yield 关键字,并且还可以规定返回值。

同样,我们可以很方便地实现斐波那契数列的迭代器生成函数

js
/**
 * 生成器函数,用于生成斐波那契数列。
 * 该函数没有参数。
 * 
 * @returns {Generator} 一个生成器,每次迭代返回下一个斐波那契数。
 */
function* fibonacci() {
	// 初始化前两个斐波那契数
	let prev1 = 0;
	let prev2 = 0;
	while (1) { // 无限循环,以生成斐波那契数列的无限个数
		// 当前值为0时,将其设置为1,并返回1
		if (prev1 == 0) {
			prev1 = 1;
			yield prev1;
		}
		// 计算下一个斐波那契数,并更新前两个数
		let temp = prev1 + prev2;
		prev2 = prev1;
		prev1 = temp;
		// 返回计算得到的新的斐波那契数
		yield prev1;
	}
}

读者在这里也能看出生成器函数的用途了,生成器函数本质上就是一个迭代器生成函数,只不过比起我们前面写的会更加方便。

生成器函数有很多细节:

  1. 生成器函数可以添加返回值,执行 return 语句后,得到的结果中 done 的值为 true,value 值为返回值。
  2. 调用生成器的 next 方法可以传递参数,传递的参数会变成 yield 表达式的返回值。基于此,第一次调用 next 函数时,传参其实没有任何意义,读者可以自行思考这是为什么。
  3. 在生成器函数内部可以调用其他生成器函数,但是要加上 *,也就是使用 yield* test() 来调用,此时就会调转到 test 函数内部去迭代。

生成器中除了 next 方法之外,还添加了很多其他方法,这里介绍 return 方法和 throw 方法,return 方法可以让生成器函数提前结束,调用该方法可以提前结束生成器函数,可以传入一个参数作为返回值的 value 属性,从而提前让整个迭代过程结束。调用 throw 方法可以在生成器中抛出一个错误,一般传入一个错误对象。

实际上,生成器更像是给函数内部打断点,使得我们可以逐次执行函数。

以笔记为镜,可以知得失