Generator 函数
1.核心知识点 总结
- Generator 是「分段执行的函数」,
function*声明,yield暂停,next()恢复执行 -
yield是暂停标记 + 返回值,yield*是遍历器委托,用于调用其他生成器 / 可遍历结构 -
next(参数)可以给「上一个 yield」传值,首次传参无效 - Generator 返回遍历器,可被
for...of遍历,return()强制终止遍历 - 核心优势:无全局变量污染、保存执行状态、外部灵活控制内部逻辑,是 ES6 异步编程的重要方案
2.什么是 Generator 函数
在Javascript中,一个函数一旦开始执行,就会运行到最后或遇到return时结束,运行期间不会有其它代码能够打断它,也不能从外部再传入值到函数体内
而Generator函数(生成器)的出现使得打破函数的完整运行成为了可能,其语法行为与传统函数完全不同
Generator函数是ES6提供的一种异步编程解决方案,形式上也是一个普通函数,但有几个显著的特征:
-- function关键字与函数名之间有一个星号 "*" (推荐紧挨着function关键字)
-- 函数体内使用 yield 表达式,定义不同的内部状态 (可以有多个yield)
-- 直接调用 Generator函数并不会执行,也不会返回运行结果,而是返回一个遍历器对象(Iterator Object)
-- 依次调用遍历器对象的next方法,遍历 Generator函数内部的每一个状态
2.1 传统函数和Generator函数区别
{
// 传统函数
function foo() {
return 'hello world'
}
foo() // 'hello world',一旦调用立即执行
// Generator函数
function* generator() {
yield 'status one' // yield 表达式是暂停执行的标记
return 'hello world'
}
let iterator = generator()
// 调用 Generator函数,函数并没有执行,返回的是一个Iterator对象
iterator.next()
// {value: "status one", done: false},value 表示返回值,done 表示遍历还没有结束
iterator.next()
// {value: "hello world", done: true},value 表示返回值,done 表示遍历结束
}
2.2 Generator函数详解
{
function* gen() {
//定义了一个 Generator函数,其中包含两个 yield 表达式和一个 return 语句(即产生了三个状态)
yield 'hello'
yield 'world'
return 'ending'
}
let it = gen()
it.next() // {value: "hello", done: false}
it.next() // {value: "world", done: false}
it.next() // {value: "ending", done: true}
it.next() // {value: undefined, done: true}
}
每次调用Iterator对象的next方法时,内部的指针就会从函数的头部或上一次停下来的地方开始执行,直到遇到下一个 yield 表达式或return语句暂停。换句话说,Generator 函数是分段执行的,yield表达式是暂停执行的标记,而 next方法可以恢复执行
执行过程如下:
第一次调用next方法时,内部指针从函数头部开始执行,遇到第一个 yield 表达式暂停,并返回当前状态的值 'hello'
第二次调用next方法时,内部指针从上一个(即第一个) yield 表达式开始,遇到第二个 yield 表达式暂停,返回当前状态的值 'world'
第三次调用next方法时,内部指针从第二个 yield 表达式开始,遇到return语句暂停,返回当前状态的值 'ending',同时所有状态遍历完毕,done 属性的值变为true
第四次调用next方法时,由于函数已经遍历运行完毕,不再有其它状态,因此返回 {value: undefined, done: true}。如果继续调用next方法,返回的也都是这个值
3.yield 表达式
(1)、yield 表达式只能用在 Generator 函数里面,用在其它地方都会报错
{
(function (){
yield 1;
})()
// SyntaxError: Unexpected number
// 在一个普通函数中使用yield表达式,结果产生一个句法错误
}
(2)、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
}
}
(3)、yield 表达式用作参数或放在赋值表达式的右边,可以不加括号
{
function* demo() {
foo(yield 'a', yield 'b'); // OK
let input = yield; // OK
}
}
(4)、yield 表达式和return语句的区别
相似:都能返回紧跟在语句后面的那个表达式的值
区别:
-- 每次遇到 yield,函数就暂停执行,下一次再从该位置继续向后执行;而 return 语句不具备记忆位置的功能
-- 一个函数只能执行一次 return 语句,而在 Generator 函数中可以有任意多个 yield
4. return() 与 throw() 方法
Generator 对象除了 next(),还有两个方法用于主动控制执行:
return(value)
- 作用:立即终止 Generator 函数,返回
{ value: 传入值, done: true }; - 后续再调用
next(),仅返回{ value: undefined, done: true }。
function* gen() {
yield 1;
yield 2;
}
const g = gen();
console.log(g.next()); // { value:1, done:false }
console.log(g.return('终止')); // { value:'终止', done:true }
console.log(g.next()); // { value:undefined, done:true }
throw(error)
- 作用:在当前暂停点抛出一个错误,若函数内未捕获,错误会向外传播;
- 若函数内用
try/catch捕获错误,函数会继续执行,直到下一个yield。
function* gen() {
try {
yield 1;
} catch (e) {
console.log('捕获错误:', e); // 捕获 throw() 抛出的错误
}
yield 2;
}
const g = gen();
console.log(g.next()); // { value:1, done:false }
g.throw(new Error('手动抛错')); // 输出:捕获错误:Error: 手动抛错
console.log(g.next()); // { value:2, done:false }
5.yield* 表达式
如果在 Generator 函数里面调用另一个 Generator 函数,默认情况下是没有效果的
{
function* foo() {
yield 'aaa'
yield 'bbb'
}
function* bar() {
foo()
yield 'ccc'
yield 'ddd'
}
let iterator = bar()
for(let value of iterator) {
console.log(value)
}
// ccc
// ddd
}
上例中,使用 for...of 来遍历函数bar的生成的遍历器对象时,只返回了bar自身的两个状态值。此时,如果想要正确的在bar 里调用foo,就需要用到 yield* 表达式
yield 表达式用来在一个 Generator 函数里面 执行 另一个 Generator 函数*
{
function* foo() {
yield 'aaa'
yield 'bbb'
}
function* bar() {
yield* foo() // 在bar函数中 **执行** foo函数
yield 'ccc'
yield 'ddd'
}
let iterator = bar()
for(let value of iterator) {
console.log(value)
}
// aaa
// bbb
// ccc
// ddd
}
6.next() 方法的参数
yield表达式本身没有返回值,或者说总是返回undefined。next方法可以带一个参数,该参数就会被当作上一个yield表达式的返回值
[rv] = yield [expression]
expression:定义通过遍历器从生成器函数返回的值,如果省略,则返回 undefined
rv:接收从下一个 next() 方法传递来的参数
例子,并尝试解析遍历生成器函数的执行过程
{
function* gen() {
let result = yield 3 + 5 + 6
console.log(result)
yield result
}
let it = gen()
console.log(it.next()) // {value: 14, done: false}
console.log(it.next()) // undefined {value: undefined, done: false}
}
第一次调用遍历器对象的next方法,函数从头部开始执行,遇到第一个 yield 暂停,在这个过程中其实是分了三步:
(1)、声明了一个变量result,并将声明提前,默认值为 undefined
(2)、由于 Generator函数是 “惰性求值”,执行到第一个 yield 时才会计算求和,并加计算结果返回给遍历器对象 {value: 14, done: false},函数暂停运行
(3)、理论上应该要把等号右边的 [yield 3 + 5 + 6] 赋值给变量result,但是, 由于函数执行到 yield 时暂定了,这一步就被挂起了
第二次调用next方法,函数从上一次 yield 停下的地方开始执行,也就是给result赋值的地方开始,由于next()并没有传参,就相当于传参为undefined
基于以上分析,就不难理解为什么说 yield表达式本身的返回值(特指 [rv])总是undefined了。现在把上面的代码稍作修改,第二次调用 next() 方法传一个参数3,按照上图分析可以很快得出输出结果
{
function* gen() {
let result = yield 3 + 5 + 6
console.log(result)
yield result
}
let it = gen()
console.log(it.next()) // {value: 14, done: false}
console.log(it.next(3)) // 3 {value: 3, done: false}
}
如果第一次调用next()的时候也传了一个参数呢?这个当然是无效的,next方法的参数表示上一个yield表达式的返回值,所以在第一次使用next方法时,传递参数是无效的。
从语义上讲,第一个next方法用来启动遍历器对象,所以不用带有参数。
{
function* gen() {
let result = yield 3 + 5 + 6
console.log(result)
yield result
}
let it = gen()
console.log(it.next(10)) // {value: 14, done: false}
console.log(it.next(3)) // 3 {value: 3, done: false}
}
Generator 函数从暂停状态到恢复运行,它的上下文状态(context)是不变的。通过next方法的参数,就有办法在 Generator 函数开始运行之后,继续向函数体内部注入值。 也就是说,可以在 Generator 函数运行的不同阶段,从外部向内部注入不同的值,从而调整函数行为。
{
function* gen(x) {
let y = 2 * (yield (x + 1))
// 注意:yield 表达式如果用在另一个表达式中,必须放在圆括号里面
let z = yield (y / 3)
return x + y + z
}
let it = gen(5)
/* 通过前面的介绍就知道这部分输出结果是错误的啦
console.log(it.next()) // {value: 6, done: false}
console.log(it.next()) // {value: 2, done: false}
console.log(it.next()) // {value: 13, done: false}
*/
/*** 正确的结果在这里 ***/
console.log(it.next())
// 首次调用next,函数只会执行到 “yield(5+1)” 暂停,并返回 {value: 6, done: false}
console.log(it.next())
// 第二次调用next,没有传递参数,
//所以 y的值是undefined,那么 y/3 当然是一个NaN,所以应该返回 {value: NaN, done: false}
console.log(it.next())
// 同样的道理,z也是undefined,6 + undefined + undefined = NaN,
//返回 {value: NaN, done: true}
}
如果向next方法提供参数,返回结果就完全不一样了
{
function* gen(x) {
let y = 2 * (yield (x + 1))
// 注意:yield 表达式如果用在另一个表达式中,必须放在圆括号里面
let z = yield (y / 3)
return x + y + z
}
let it = gen(5)
console.log(it.next())
// 正常的运算应该是先执行圆括号内的计算,再去乘以2,
//由于圆括号内被 yield 返回 5 + 1 的结果并暂停,所以返回{value: 6, done: false}
console.log(it.next(9))
// 上次是在圆括号内部暂停的,所以第二次调用 next方法应该从圆括号里面开始,
//就变成了 let y = 2 * (9),y被赋值为18,
//所以第二次返回的应该是 18/3的结果 {value: 6, done: false}
console.log(it.next(2))
// 参数2被赋值给了 z,最终 x + y + z = 5 + 18 + 2 = 25,返回 {value: 25, done: true}
}
{
function* gen(x) {
let y = 2 * (yield (x + 1))
let z = yield (y / 3)
z = 88 // 注意看这里
return x + y + z
}
let it = gen(5)
console.log(it.next()) // {value: 6, done: false}
console.log(it.next(9)) // {value: 6, done: false}
console.log(it.next(2)) // 这里其实也很容易理解,参数2被赋值给了 z,但是函数体内又给 z 重新赋值为88, 最终 x + y + z = 5 + 18 + 88 = 111,返回 {value: 111, done: true}
}
7.Generator函数与 Iterator 接口的关系
7.1Generator 函数的核心用途之一是简化迭代器的创建
- Generator 对象本身就是一个迭代器(实现了
Symbol.iterator方法,且返回自身); - 普通迭代器需要手动实现
next()方法和状态管理,而 Generator 用yield即可自动实现迭代逻辑。
(1). 手动实现迭代器(繁琐)
// 手动创建一个迭代器,生成 1~3 的数字
const iterator = {
count: 1,
next() {
if (this.count <= 3) {
return { value: this.count++, done: false };
} else {
return { value: undefined, done: true };
}
},
[Symbol.iterator]() { return this; } // 实现可迭代协议
};
// 迭代
for (const val of iterator) {
console.log(val); // 1, 2, 3
}
(2). Generator 实现迭代器(简洁)
// Generator 自动生成迭代器
function* numberGenerator() {
yield 1;
yield 2;
yield 3;
}
// 迭代(支持 for...of,因为 Generator 对象是可迭代的)
for (const val of numberGenerator()) {
console.log(val); // 1, 2, 3
}
7.2 Iterator(迭代器)
JavaScript原有的表示集合的数据结构有数组(Array)和对象(Object),ES6又添加了Map和Set。这样就有了4种数据集合,此时便需要****一种统一的接口机制来处理不同的数据结构 。
ES6 规定,默认的 Iterator 接口部署在数据结构的Symbol.iterator属性,或者说,一个数据结构只要具有Symbol.iterator属性,就可以认为是“可遍历的”(iterable)。
Symbol.iterator属性本身是一个函数,就是当前数据结构默认的遍历器生成函数。执行这个函数,就会返回一个遍历器。
传统对象没有原生部署 Iterator接口,不能使用 for...of 和 扩展运算符,现在通过给对象添加 Symbol.iterator 属性和对应的遍历器生成函数,就可以使用了
Iterator对象
- Iterator就是这样一个统一的接口。任何数据结构,主要部署Iterator接口,就可以完成遍历。
- Iterator接口主要供for...of使用(ES6创造的新的遍历命令),当使用for...of循环时,该循环会自动寻找Iterator接口
- Iterator对象本质上是一个指针对象。(创建时指向数据结构头部,依次调用next()方法后指针会移动,依次指向第1,2,3...个成员,最后指向结束位置)
默认迭代器
const obj = {//obj具有Symbol.iterator(它是一个方法),因此是可遍历的
[Symbol.iterator]:function(){
return {
next:function(){
return {
value:1,
done:true
}
}
}
}
}
ES6的有些数据结构(数组)原生部署了Symbol.iterator属性(称为部署了 遍历器接口 ),即不用任何处理就可以被for...of循环。另外一些数据结构(对象)没有。
以下数据结构原生部署Iterator接口:也就是说这些都可以使用for...of。除了这些,其他数据结构(如对象)的Iterator接口需要自己在Symbol.iterator属性上面部署,才会被for...of遍历。
- Map
- Set
//NodeList对象//数组的默认迭代器:
let color = ['red','yellow','blue']
let arrIt = colorSymbol.iterator;//返回一个迭代器
arrIt.next()//{value:'red',done:false}
//类数组arguments的默认迭代器:
function fn(){
let argsIt = argumentsSymbol.iterator;
argsIt.next()
}
//类数组dom节点的默认迭代器:
let myP = document.getElementsByTagName('li');
let pIt = myPSymbol.iterator;
pIt.next();
//字符串的默认迭代器:
let str = 'dhakjda';
let strIt = strSymbol.iterator;
strIt.next();
//对象没有默认(即内置)迭代器:obj[Symbol.iterator] is not a function
8.for...of 循环
由于 Generator 函数运行时生成的是一个 Iterator 对象,因此,可以直接使用 for...of 循环遍历,且此时无需再调用 next() 方法
这里需要注意,一旦 next() 方法的返回对象的 done 属性为 true,for...of 循环就会终止,且不包含该返回对象
{
function* gen() {
yield 1
yield 2
yield 3
yield 4
return 5
}
for(let item of gen()) {
console.log(item)
}
// 1 2 3 4
}
9.Generator 的典型应用场景
9.1 异步编程(ES6 时代的方案)
Generator 是 async/await 的 “前身” ,通过 yield 暂停异步操作,next() 恢复执行,解决了回调地狱问题。需配合自动执行器(如 co 库)使用:
// 模拟异步请求
function fetchData(url) {
return new Promise(resolve => {
setTimeout(() => resolve(`数据:${url}`), 1000);
});
}
// Generator 函数封装异步逻辑
function* asyncGenerator() {
const data1 = yield fetchData('url1');
console.log(data1); // 1秒后输出:数据:url1
const data2 = yield fetchData('url2');
console.log(data2); // 再1秒后输出:数据:url2
}
// 手动执行(实际用 co 库自动执行)
const gen = asyncGenerator();
gen.next().value.then(data1 => {
gen.next(data1).value.then(data2 => {
gen.next(data2);
});
});
//.then(data1 => {}) → 是 Promise 的异步回调,等 1 秒后,异步请求完成,
//Promise 的 resolve 值是 数据:url1,这个值会被自动传给回调函数的形参 data1;
注:ES7 引入的 async/await 是 Generator + Promise 的语法糖,更简洁易用,现在已替代 Generator 成为主流异步方案
9.2 生成无限序列(惰性求值)
Generator 支持 “按需生成” 数据,不会一次性创建所有数据,适合处理无限序列(如斐波那契数列)或大数据集:
// 生成无限斐波那契数列
function* fibonacci() {
let a = 0, b = 1;
while (true) {
yield a;
[a, b] = [b, a + b];
}
}
const fib = fibonacci();
console.log(fib.next().value); // 0
console.log(fib.next().value); // 1
console.log(fib.next().value); // 1
console.log(fib.next().value); // 2
console.log(fib.next().value); // 3
// 按需获取,不会占用大量内存
9.3 控制流管理
通过 yield 可以灵活控制函数执行顺序,适合复杂的流程控制(如分步执行、条件分支):
function* taskFlow() {
console.log('任务1');
yield; // 暂停,等待外部触发下一步
console.log('任务2');
const flag = yield '是否执行任务3?'; // 产出询问,接收外部决策
if (flag) {
console.log('任务3执行');
} else {
console.log('任务3跳过');
}
}
const flow = taskFlow();
flow.next(); // 任务1 → { value: undefined, done: false }
const res = flow.next(); // 任务2 → { value: '是否执行任务3?', done: false }
flow.next(true); // 任务3执行 → { value: undefined, done: true }
最后
这是《JavaScript系列》第8篇,将持续更新。
小伙伴如果喜欢我的分享,可以动动您发财的手关注下我,我会持续更新的!!!
您对我的关注、点赞和收藏,是对我最大的支持!欢迎关注、评论、讨论和指正!