JS `Generator` 实现无限序列:如斐波那契数列

各位观众老爷们,今天咱们来聊聊 JavaScript 里一个挺有意思的东西:Generator,以及怎么用它来实现无限序列,比如说那个著名的斐波那契数列。这玩意儿听起来高大上,其实理解起来也不难,咱们争取用最接地气的方式把它讲明白。

开场白:什么是 Generator?

先别急着斐波那契,咱们得先搞清楚 Generator 是个什么玩意儿。你可以把它想象成一个“暂停按钮”,普通的函数一运行就一口气跑到底,而 Generator 函数可以在运行过程中停下来,等你喊“开始”的时候再继续跑。

这“暂停”和“继续”的关键在于 yield 关键字。yield 就像一个路标,告诉 Generator 函数:到这儿就歇会儿,把后面的值吐出去。

Generator 函数的基本语法

Generator 函数的定义和普通函数差不多,只不过多了一个星号 *

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

const generator = myGenerator(); // 注意!这里并没有执行函数体,而是返回一个 Generator 对象

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

咱们来解读一下这段代码:

  1. function* myGenerator(): 定义了一个 Generator 函数。注意那个星号 *,它告诉 JavaScript 引擎,这是一个特殊的函数。
  2. const generator = myGenerator();: 调用 Generator 函数,注意! 这时候函数体并没有执行,而是返回一个 Generator 对象。这个对象就像一个迭代器,可以控制函数的执行。
  3. generator.next(): 调用 Generator 对象的 next() 方法,开始或继续执行 Generator 函数。每次调用 next(),函数会执行到下一个 yield 语句,然后暂停,并返回一个对象,包含两个属性:
    • valueyield 后面表达式的值。
    • done: 一个布尔值,表示 Generator 函数是否执行完毕。false 表示还没完,true 表示已经结束了。

用 Generator 实现斐波那契数列

现在,咱们进入正题,看看怎么用 Generator 实现斐波那契数列。 斐波那契数列的定义是:第一项和第二项都是 1,从第三项开始,每一项都是前两项的和。 也就是:1, 1, 2, 3, 5, 8, 13, 21, …

function* fibonacci() {
  let a = 1;
  let b = 1;

  while (true) { // 无限循环!
    yield a;
    [a, b] = [b, a + b]; // ES6 的解构赋值,简洁明了
  }
}

const fib = fibonacci();

console.log(fib.next().value); // 1
console.log(fib.next().value); // 1
console.log(fib.next().value); // 2
console.log(fib.next().value); // 3
console.log(fib.next().value); // 5
console.log(fib.next().value); // 8
// ... 可以一直调用下去

这段代码的核心在于 while (true) 循环,这意味着这个 Generator 函数可以无限地生成斐波那契数列的项。

咱们来分析一下:

  1. let a = 1; let b = 1;: 初始化斐波那契数列的前两项。
  2. while (true): 创建一个无限循环。
  3. yield a;yield 当前的斐波那契数 a
  4. [a, b] = [b, a + b];: 用 ES6 的解构赋值更新 ab,计算下一个斐波那契数。 相当于:
    let temp = b;
    b = a + b;
    a = temp;
  5. const fib = fibonacci();: 创建一个斐波那契数列的 Generator 对象。
  6. fib.next().value: 调用 next() 方法,获取下一个斐波那契数。

Generator 的优势:惰性求值

Generator 最大的优势在于惰性求值(Lazy Evaluation)。 也就是说,它不会一次性计算出所有的斐波那契数,而是只有在你调用 next() 方法的时候,才会计算并返回下一个数。

这对于无限序列来说非常重要。 如果不用 Generator,而是直接用数组存储斐波那契数列,那么程序很快就会耗尽内存。

Generator 的应用场景

除了生成无限序列,Generator 还有很多其他的应用场景:

  • 异步编程: 可以用 Generator 来简化异步操作,配合 async/await 语法,可以让异步代码看起来像同步代码一样。
  • 状态管理: 可以用 Generator 来管理复杂的状态,例如游戏的状态机。
  • 数据流处理: 可以用 Generator 来处理大量的数据流,例如读取文件或网络数据。

Generator 的进阶用法

Generator 还有一些进阶用法,比如:

  • *`yield委托**: 可以将一个Generator函数委托给另一个Generator` 函数。
  • next() 方法传参: 可以给 next() 方法传递参数,这个参数会作为上一个 yield 表达式的返回值。
  • return()throw() 方法: 可以提前结束 Generator 函数,或者抛出一个错误。

咱们先来看 yield* 的用法:

function* anotherGenerator() {
  yield 4;
  yield 5;
  yield 6;
}

function* myGenerator() {
  yield 1;
  yield 2;
  yield 3;
  yield* anotherGenerator(); // 委托给 anotherGenerator
  yield 7;
  yield 8;
}

const generator = myGenerator();

console.log(generator.next()); // { value: 1, done: false }
console.log(generator.next()); // { value: 2, done: false }
console.log(generator.next()); // { value: 3, done: false }
console.log(generator.next()); // { value: 4, done: false }
console.log(generator.next()); // { value: 5, done: false }
console.log(generator.next()); // { value: 6, done: false }
console.log(generator.next()); // { value: 7, done: false }
console.log(generator.next()); // { value: 8, done: false }
console.log(generator.next()); // { value: undefined, done: true }

yield* anotherGenerator() 会将 anotherGenerator 生成的值依次 yield 出来,就像把 anotherGenerator 的代码直接复制到 myGenerator 里一样。

再来看看 next() 传参的例子:

function* myGenerator() {
  const result = yield 'What is your name?';
  yield `Hello, ${result}!`;
}

const generator = myGenerator();

console.log(generator.next()); // { value: 'What is your name?', done: false }
console.log(generator.next('Alice')); // { value: 'Hello, Alice!', done: false }
console.log(generator.next()); // { value: undefined, done: true }

第一次调用 next() 时,Generator 函数执行到 yield 'What is your name?',暂停并返回字符串 "What is your name?"。

第二次调用 next('Alice') 时,字符串 "Alice" 会作为上一个 yield 表达式 yield 'What is your name?' 的返回值,赋值给变量 result。 然后,Generator 函数继续执行,yield Hello, ${result}!“,返回字符串 "Hello, Alice!"。

最后,return()throw() 方法分别用于提前结束 Generator 函数和抛出错误。

function* myGenerator() {
  try {
    yield 1;
    yield 2;
    yield 3;
  } finally {
    console.log('Cleaning up...');
  }
}

const generator = myGenerator();

console.log(generator.next()); // { value: 1, done: false }
console.log(generator.return('Finished')); // { value: 'Finished', done: true }
// Cleaning up...

function* myGenerator2() {
  try {
    yield 1;
    yield 2;
    yield 3;
  } finally {
    console.log('Cleaning up...');
  }
}

const generator2 = myGenerator2();

console.log(generator2.next()); // { value: 1, done: false }
try {
  generator2.throw(new Error('Something went wrong!'));
} catch (e) {
  console.error(e); // Error: Something went wrong!
}
// Cleaning up...

return() 方法会立即结束 Generator 函数,并返回一个 donetrue 的对象。 finally 块中的代码会被执行。

throw() 方法会抛出一个错误,可以在 try...catch 块中捕获。 finally 块中的代码也会被执行。

总结

咱们今天主要讲了 Generator 函数的基本概念和用法,以及如何用它来实现无限序列,例如斐波那契数列。 Generator 的核心在于 yield 关键字,它可以让函数在执行过程中暂停和继续。 Generator 的优势在于惰性求值,可以避免一次性计算大量数据,节省内存。

Generator 在异步编程、状态管理、数据流处理等方面都有广泛的应用。 yield*next() 传参、return()throw() 方法可以让我们更灵活地控制 Generator 函数的执行。

希望今天的讲解对大家有所帮助。 如果有什么问题,欢迎提问。 祝大家编程愉快!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注