MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

JavaScript生成器基础入门

2022-07-147.0k 阅读

JavaScript 生成器基础入门

什么是生成器

在 JavaScript 中,生成器(Generator)是一种特殊的函数,它可以暂停和恢复执行,为异步编程和迭代操作提供了一种强大而灵活的方式。生成器函数通过 function* 语法定义,与普通函数最大的区别在于,生成器函数执行时不会一次性运行到结束,而是可以通过 yield 关键字暂停函数执行,并返回一个值。

普通函数一旦开始执行,会一直运行直到返回一个值或者抛出异常,其执行过程是连续的、不可中断的。而生成器函数就像是一个“可暂停的函数”,每次遇到 yield 语句时,函数就会暂停执行,将 yield 后面的值返回给调用者。调用者可以通过调用生成器对象的 next() 方法来恢复函数的执行,从暂停的地方继续执行,直到遇到下一个 yield 语句或者函数结束。

生成器函数的定义

生成器函数的定义使用 function* 关键字,后面跟着函数名和参数列表,与普通函数的定义形式类似,但多了一个 * 号。例如:

function* myGenerator() {
    yield 1;
    yield 2;
    yield 3;
}

在上述代码中,myGenerator 就是一个生成器函数。当调用这个函数时,它并不会立即执行函数体中的代码,而是返回一个生成器对象。我们可以通过调用生成器对象的 next() 方法来启动生成器函数的执行。

生成器对象与 next() 方法

调用生成器函数会返回一个生成器对象,该对象具有 next() 方法。每次调用 next() 方法时,生成器函数会从上次暂停的地方继续执行,直到遇到下一个 yield 语句或者函数结束。next() 方法返回一个对象,该对象包含两个属性:valuedonevalue 属性表示 yield 语句返回的值,done 属性是一个布尔值,表示生成器是否已经完成(即是否已经执行到函数末尾)。

以下是一个完整的示例,展示如何使用生成器函数和生成器对象:

function* myGenerator() {
    yield 1;
    yield 2;
    yield 3;
}

let gen = myGenerator();

console.log(gen.next()); // { value: 1, done: false }
console.log(gen.next()); // { value: 2, done: false }
console.log(gen.next()); // { value: 3, done: false }
console.log(gen.next()); // { value: undefined, done: true }

在上述代码中,我们首先定义了 myGenerator 生成器函数,然后调用它并将返回的生成器对象赋值给 gen。每次调用 gen.next() 时,生成器函数从上次暂停的地方继续执行,遇到 yield 语句时暂停并返回相应的值。当生成器函数执行完毕(没有更多的 yield 语句),next() 方法返回的 done 属性为 truevalue 属性为 undefined

yield 关键字

yield 关键字是生成器函数的核心特性之一,它用于暂停生成器函数的执行,并返回一个值给调用者。yield 后面可以跟任意表达式,该表达式的值就是 next() 方法返回的 value 属性的值。

例如,我们可以在 yield 后面使用变量、函数调用等:

function* myGenerator() {
    let num = 1;
    yield num;
    num = num * 2;
    yield num;
    num = num * 2;
    yield num;
}

let gen = myGenerator();

console.log(gen.next()); // { value: 1, done: false }
console.log(gen.next()); // { value: 2, done: false }
console.log(gen.next()); // { value: 4, done: false }
console.log(gen.next()); // { value: undefined, done: true }

在这个例子中,每次 yield 语句返回的值都依赖于之前的计算结果。

生成器与迭代器

生成器与迭代器(Iterator)密切相关。实际上,生成器对象本身就是一个迭代器,它实现了迭代器协议,即具有 next() 方法,并且该方法返回的对象具有 valuedone 属性。

这意味着我们可以在需要迭代器的地方使用生成器对象,例如在 for...of 循环中:

function* myGenerator() {
    yield 1;
    yield 2;
    yield 3;
}

let gen = myGenerator();

for (let value of gen) {
    console.log(value);
}
// 输出:
// 1
// 2
// 3

在上述代码中,for...of 循环会自动调用生成器对象的 next() 方法,直到 done 属性为 true。每次循环时,for...of 会将 next() 方法返回的 value 属性值赋给 value 变量,并执行循环体。

生成器函数的参数

生成器函数可以接受参数,就像普通函数一样。这些参数在调用生成器函数时传递,并且可以在生成器函数内部使用。

function* myGenerator(num) {
    yield num;
    yield num * 2;
    yield num * 3;
}

let gen = myGenerator(5);

console.log(gen.next()); // { value: 5, done: false }
console.log(gen.next()); // { value: 10, done: false }
console.log(gen.next()); // { value: 15, done: false }
console.log(gen.next()); // { value: undefined, done: true }

在这个例子中,我们将数字 5 作为参数传递给 myGenerator 生成器函数,然后生成器函数根据这个参数进行计算并通过 yield 返回不同的值。

在生成器函数内部使用 next() 方法的返回值

next() 方法不仅可以启动生成器函数的执行,还可以向生成器函数内部传递值。当调用 next() 方法并传入一个值时,这个值会作为上一个 yield 语句的返回值。

function* myGenerator() {
    let value1 = yield '初始值';
    console.log('接收到的值 1:', value1);
    let value2 = yield value1 * 2;
    console.log('接收到的值 2:', value2);
    return value2 * 3;
}

let gen = myGenerator();

console.log(gen.next()); // { value: '初始值', done: false }
console.log(gen.next(10)); // { value: 20, done: false }  这里传入的 10 会作为第一个 yield 的返回值
console.log(gen.next(20)); // { value: 60, done: true }   这里传入的 20 会作为第二个 yield 的返回值

在上述代码中,第一次调用 gen.next() 时,生成器函数执行到第一个 yield 语句暂停,并返回 '初始值'。第二次调用 gen.next(10) 时,10 会作为第一个 yield 语句的返回值,赋值给 value1,然后生成器函数继续执行到第二个 yield 语句暂停,并返回 value1 * 2(即 20)。第三次调用 gen.next(20) 时,20 会作为第二个 yield 语句的返回值,赋值给 value2,然后生成器函数执行完毕并返回 value2 * 3(即 60)。

生成器的高级应用 - 异步操作与迭代

生成器在异步编程中有着非常重要的应用。传统的异步操作,如使用回调函数或 Promise,在处理复杂的异步流程时可能会导致代码变得难以阅读和维护。而生成器可以通过暂停和恢复执行的特性,让异步操作看起来更像是同步代码。

例如,我们可以使用生成器来模拟一个简单的异步操作序列:

function delay(ms) {
    return new Promise(resolve => setTimeout(resolve, ms));
}

function* asyncTasks() {
    console.log('开始任务 1');
    yield delay(1000);
    console.log('任务 1 完成');
    yield delay(1500);
    console.log('任务 2 完成');
}

let tasks = asyncTasks();
tasks.next();
setTimeout(() => tasks.next(), 1000);
setTimeout(() => tasks.next(), 2500);

在上述代码中,delay 函数返回一个 Promise,模拟了一个延迟操作。asyncTasks 是一个生成器函数,其中通过 yield 暂停执行并等待 Promise 被解决。通过手动调用 next() 方法,我们可以控制异步任务的执行流程,使得异步操作看起来更像是顺序执行的同步代码。

然而,手动调用 next() 方法来处理异步操作还是比较繁琐的。为了更方便地使用生成器处理异步操作,我们可以使用 co 库或者 async/await 语法(async/await 本质上也是基于生成器实现的)。

使用 co 库简化异步生成器操作

co 库是一个用于生成器的自动执行器,它可以自动处理生成器中的 yield 语句,使得异步操作更加简洁。

首先,我们需要安装 co 库:

npm install co

然后,我们可以使用 co 来改写上面的异步任务示例:

const co = require('co');

function delay(ms) {
    return new Promise(resolve => setTimeout(resolve, ms));
}

function* asyncTasks() {
    console.log('开始任务 1');
    yield delay(1000);
    console.log('任务 1 完成');
    yield delay(1500);
    console.log('任务 2 完成');
}

co(asyncTasks()).then(() => {
    console.log('所有任务完成');
});

在上述代码中,co(asyncTasks()) 会自动执行 asyncTasks 生成器函数,每当遇到 yield 语句时,它会等待对应的 Promise 被解决,然后继续执行生成器函数。当生成器函数执行完毕,co 返回的 Promise 也会被解决。

async/await 语法与生成器的关系

async/await 语法是 ES2017 引入的异步编程语法糖,它实际上是基于生成器和 Promise 实现的。async 函数本质上就是一个返回 Promise 的生成器函数的语法糖。

async 函数内部可以使用 await 关键字暂停函数执行,直到一个 Promise 被解决(或被拒绝)。await 只能在 async 函数内部使用,这与生成器函数中的 yield 类似。

以下是一个使用 async/await 实现的异步任务示例:

function delay(ms) {
    return new Promise(resolve => setTimeout(resolve, ms));
}

async function asyncTasks() {
    console.log('开始任务 1');
    await delay(1000);
    console.log('任务 1 完成');
    await delay(1500);
    console.log('任务 2 完成');
}

asyncTasks().then(() => {
    console.log('所有任务完成');
});

在上述代码中,asyncTasks 是一个 async 函数,其中使用 await 暂停函数执行,等待 delay 返回的 Promise 被解决。async 函数会自动将返回值包装成一个 Promise,使得异步操作更加简洁和直观。

生成器的错误处理

与普通函数一样,生成器函数也需要处理错误。在生成器中,错误可以通过 try...catch 块来捕获,也可以通过生成器对象的 throw() 方法抛出。

使用 try...catch 捕获错误

function* myGenerator() {
    try {
        yield 1;
        throw new Error('发生错误');
        yield 2;
    } catch (error) {
        console.log('捕获到错误:', error.message);
    }
    yield 3;
}

let gen = myGenerator();
console.log(gen.next()); // { value: 1, done: false }
try {
    gen.next();
} catch (error) {
    console.log('外部捕获到错误:', error.message);
}
console.log(gen.next()); // { value: 3, done: false }
console.log(gen.next()); // { value: undefined, done: true }

在上述代码中,当生成器函数执行到 throw new Error('发生错误') 时,会抛出一个错误。在生成器函数内部的 try...catch 块可以捕获这个错误,并进行相应的处理。如果在生成器函数外部调用 next() 方法时,生成器函数内部抛出的错误没有被捕获,那么外部也可以通过 try...catch 块来捕获这个错误。

使用 throw() 方法抛出错误

生成器对象的 throw() 方法可以向生成器函数内部抛出一个错误,并且会从当前暂停的 yield 语句处开始恢复执行,就像执行了 throw 语句一样。

function* myGenerator() {
    try {
        yield 1;
        yield 2;
    } catch (error) {
        console.log('捕获到错误:', error.message);
    }
    yield 3;
}

let gen = myGenerator();
console.log(gen.next()); // { value: 1, done: false }
console.log(gen.next()); // { value: 2, done: false }
try {
    gen.throw(new Error('外部抛出的错误'));
} catch (error) {
    console.log('外部捕获到错误:', error.message);
}
console.log(gen.next()); // { value: 3, done: false }
console.log(gen.next()); // { value: undefined, done: true }

在这个例子中,我们通过 gen.throw(new Error('外部抛出的错误')) 向生成器函数内部抛出一个错误。生成器函数内部的 try...catch 块捕获到这个错误并进行处理。然后生成器函数继续执行,直到遇到下一个 yield 语句或者函数结束。

生成器与可迭代对象

生成器函数返回的生成器对象是一个可迭代对象,这意味着我们可以使用 for...of 循环、Array.from() 等方法来操作它。此外,我们还可以自定义可迭代对象,并使用生成器来实现其迭代逻辑。

使用生成器实现自定义可迭代对象

const myIterable = {
    *[Symbol.iterator]() {
        yield 1;
        yield 2;
        yield 3;
    }
};

for (let value of myIterable) {
    console.log(value);
}
// 输出:
// 1
// 2
// 3

在上述代码中,我们定义了一个 myIterable 对象,并通过在该对象上定义 Symbol.iterator 方法来使其成为一个可迭代对象。Symbol.iterator 方法是一个生成器函数,它定义了对象的迭代逻辑。通过 for...of 循环,我们可以方便地遍历这个自定义可迭代对象。

使用 Array.from() 方法将生成器转换为数组

function* myGenerator() {
    yield 1;
    yield 2;
    yield 3;
}

let arr = Array.from(myGenerator());
console.log(arr); // [1, 2, 3]

在这个例子中,Array.from() 方法接受一个可迭代对象(这里是生成器对象)作为参数,并将其转换为一个数组。这在需要将生成器生成的值收集到一个数组中进行进一步处理时非常有用。

生成器的递归应用

生成器函数可以进行递归调用,这在处理一些需要递归逻辑的场景时非常有用。例如,我们可以使用递归生成器来生成斐波那契数列。

function* fibonacciGenerator(n) {
    if (n <= 0) return;
    if (n === 1) {
        yield 0;
        return;
    }
    if (n === 2) {
        yield 0;
        yield 1;
        return;
    }
    let gen1 = fibonacciGenerator(n - 1);
    let gen2 = fibonacciGenerator(n - 2);
    let prev1 = gen1.next();
    let prev2 = gen2.next();
    yield prev1.value;
    yield prev2.value;
    while (!gen1.done &&!gen2.done) {
        let sum = prev1.value + prev2.value;
        yield sum;
        prev2 = prev1;
        prev1 = gen1.next();
        if (gen1.done) {
            prev1 = prev2;
            prev2 = { value: 0, done: true };
        }
    }
}

let fibGen = fibonacciGenerator(10);
for (let num of fibGen) {
    console.log(num);
}

在上述代码中,fibonacciGenerator 是一个递归生成器函数,用于生成指定长度的斐波那契数列。通过递归调用自身,并结合 yield 语句,我们可以逐步生成斐波那契数列中的每一项。

生成器与模块化

在模块化编程中,生成器也可以发挥作用。例如,我们可以在模块中定义生成器函数,然后在其他模块中调用这些生成器函数,以实现更灵活的代码组织和复用。

定义生成器函数模块

假设我们有一个 generatorModule.js 文件:

// generatorModule.js
function* myGenerator() {
    yield 1;
    yield 2;
    yield 3;
}

export { myGenerator };

使用生成器函数模块

在另一个文件 main.js 中:

// main.js
import { myGenerator } from './generatorModule.js';

let gen = myGenerator();
for (let value of gen) {
    console.log(value);
}
// 输出:
// 1
// 2
// 3

在上述代码中,我们在 generatorModule.js 模块中定义了 myGenerator 生成器函数,并通过 export 将其导出。在 main.js 模块中,我们使用 import 导入 myGenerator 函数,并在该模块中使用它。

生成器的性能考虑

虽然生成器提供了强大的功能,但在使用时也需要考虑性能问题。由于生成器函数的执行过程涉及暂停和恢复,与普通函数相比,可能会有一定的性能开销。

在处理大量数据或者对性能要求较高的场景下,需要谨慎使用生成器。例如,在一些简单的数组遍历场景中,使用普通的 for 循环可能会比使用生成器结合 for...of 循环更高效,因为普通 for 循环没有生成器的暂停和恢复开销。

然而,在处理异步操作或者需要按需生成数据的场景下,生成器的优势就会体现出来,它可以避免一次性加载大量数据,从而提高内存使用效率。

总结

生成器是 JavaScript 中一个强大而灵活的特性,它为异步编程、迭代操作以及代码控制流提供了新的思路和方法。通过 function* 定义生成器函数,使用 yield 关键字暂停和返回值,生成器对象及其 next() 方法实现函数的暂停和恢复执行。生成器在异步编程中与 Promise 相结合,通过 co 库或者 async/await 语法糖,使得异步代码更加简洁易读。同时,生成器在处理自定义可迭代对象、递归操作以及模块化编程等方面也有着广泛的应用。虽然在性能方面需要根据具体场景进行权衡,但总体来说,掌握生成器对于深入理解 JavaScript 语言以及编写高效、灵活的代码至关重要。