JS `Generator` 函数 (`function*`):创建自定义迭代器与惰性求值

各位听众,大家好!欢迎来到今天的"JS Generator函数:创建自定义迭代器与惰性求值"专题讲座。今天咱们不搞那些虚头巴脑的,直接上干货,一起探索一下 JavaScript 中这个有点儿神秘,但其实超级有用的 Generator 函数。

一、什么是 Generator 函数?

首先,别被 Generator 这个名字吓到,它其实没那么复杂。你可以把它想象成一个“暂停”按钮加强版的函数。普通的函数一旦开始执行,要么一口气执行完,要么就报错,中间没得停。但是 Generator 函数不一样,它可以在执行过程中“暂停”多次,并且每次暂停的时候还可以给你“吐”出一个值。

怎么定义一个 Generator 函数呢?很简单,就是在 function 关键字后面加个小星星 * 就行了。

function* myGenerator() {
  console.log("开始执行...");
  yield 1;  // 暂停,并返回 1
  console.log("继续执行...");
  yield 2;  // 暂停,并返回 2
  console.log("执行结束");
}

看到 yield 关键字了吗?这就是 Generator 函数暂停和返回值的关键。每次遇到 yield,函数就会暂停执行,并将 yield 后面的表达式的值返回。

二、Generator 函数的调用与迭代器

Generator 函数和普通函数还有一个很大的区别:调用 Generator 函数不会立即执行函数体内的代码,而是会返回一个迭代器对象

const iterator = myGenerator(); // 注意,这里没有执行函数体内的代码

console.log(iterator); // 输出: Object [Generator] {}

这个迭代器对象有一个 next() 方法,调用 next() 方法才会真正开始执行 Generator 函数体内的代码,直到遇到第一个 yield

console.log(iterator.next()); // 输出: { value: 1, done: false }
// 控制台输出: 开始执行...

console.log(iterator.next()); // 输出: { value: 2, done: false }
// 控制台输出: 继续执行...

console.log(iterator.next()); // 输出: { value: undefined, done: true }
// 控制台输出: 执行结束

每次调用 next() 方法,都会返回一个对象,包含两个属性:

  • value: yield 关键字后面的表达式的值。
  • done: 一个布尔值,表示 Generator 函数是否已经执行完毕。donetrue 时,表示函数已经执行到最后,没有更多的 yield 了。

三、Generator 函数的参数传递

next() 方法还可以接受一个参数,这个参数会被传递给上一个 yield 表达式的返回值。这听起来有点绕,我们来看个例子:

function* myGeneratorWithInput() {
  const value1 = yield "What's your name?";
  console.log("My name is:", value1);
  const value2 = yield "How old are you?";
  console.log("I'm", value2, "years old.");
}

const iterator2 = myGeneratorWithInput();

console.log(iterator2.next()); // 输出: { value: "What's your name?", done: false }
// 控制台无输出

console.log(iterator2.next("Alice")); // 输出: { value: "How old are you?", done: false }
// 控制台输出: My name is: Alice

console.log(iterator2.next(30)); // 输出: { value: undefined, done: true }
// 控制台输出: I'm 30 years old.

在这个例子中,第一次调用 next() 方法时,Generator 函数暂停在第一个 yield 表达式,并返回 "What's your name?"。第二次调用 next("Alice") 时,"Alice" 会被赋值给 value1,然后继续执行,直到遇到第二个 yield

四、Generator 函数的 return 方法

除了 next() 方法,迭代器对象还有一个 return() 方法。调用 return() 方法会强制结束 Generator 函数的执行,并返回一个指定的值。

function* myGeneratorWithReturn() {
  yield 1;
  yield 2;
  yield 3;
  return "Done!"; // 显式返回值
}

const iterator3 = myGeneratorWithReturn();

console.log(iterator3.next()); // 输出: { value: 1, done: false }
console.log(iterator3.return("Forced Exit!")); // 输出: { value: "Forced Exit!", done: true }
console.log(iterator3.next()); // 输出: { value: undefined, done: true }

注意,在调用 return() 方法之后,再调用 next() 方法,done 属性始终为 true

五、Generator 函数的 throw() 方法

throw() 方法用于在 Generator 函数内部抛出一个异常。

function* myGeneratorWithThrow() {
  try {
    yield 1;
    yield 2;
    yield 3;
  } catch (error) {
    console.error("Caught an error:", error);
  }
}

const iterator4 = myGeneratorWithThrow();

console.log(iterator4.next()); // 输出: { value: 1, done: false }
iterator4.throw(new Error("Something went wrong!")); // 抛出异常
// 控制台输出: Caught an error: Error: Something went wrong!
console.log(iterator4.next()); // 输出: { value: undefined, done: true }

如果在 Generator 函数内部没有捕获这个异常,那么这个异常会一直冒泡到调用栈的顶层。

六、Generator 函数的应用场景

说了这么多,Generator 函数到底有什么用呢?其实它的应用场景非常广泛,主要集中在以下几个方面:

  1. 自定义迭代器

    Generator 函数最常见的用途就是创建自定义迭代器。我们可以用它来遍历任何数据结构,而不仅仅是数组或对象。

    例如,我们可以创建一个迭代器来遍历二叉树:

    class TreeNode {
      constructor(value) {
        this.value = value;
        this.left = null;
        this.right = null;
      }
    }
    
    function* traverseTree(node) {
      if (node) {
        yield* traverseTree(node.left);
        yield node.value;
        yield* traverseTree(node.right);
      }
    }
    
    const root = new TreeNode(1);
    root.left = new TreeNode(2);
    root.right = new TreeNode(3);
    root.left.left = new TreeNode(4);
    root.left.right = new TreeNode(5);
    
    const treeIterator = traverseTree(root);
    
    for (const value of treeIterator) {
      console.log(value); // 输出: 4 2 5 1 3
    }

    在这个例子中,traverseTree 函数使用递归的方式遍历二叉树,并使用 yield* 关键字来委托给子树的迭代器。

  2. 惰性求值

    Generator 函数可以实现惰性求值,也就是只有在需要的时候才计算结果。这对于处理大数据集或者无限序列非常有用。

    例如,我们可以创建一个生成斐波那契数列的 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
    console.log(fib.next().value); // 输出: 5

    这个 fibonacci 函数会无限地生成斐波那契数列,但是只有在调用 next() 方法时才会计算下一个值。

  3. 异步编程

    Generator 函数可以和 async/await 关键字一起使用,简化异步编程。这部分内容比较高级,我们会在后面的讲座中详细讲解。

  4. 状态管理

    Generator 函数可以用来管理复杂的状态,例如游戏的状态机。

*七、`yield` 的用法**

上面我们提到了 yield* 关键字,它有什么作用呢?yield* 实际上是 yield 的一种增强形式,它可以将迭代器的控制权委托给另一个迭代器。

function* generator1() {
  yield 1;
  yield 2;
}

function* generator2() {
  yield* generator1(); // 将控制权委托给 generator1
  yield 3;
  yield 4;
}

const iterator5 = generator2();

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

在这个例子中,generator2 使用 yield* generator1() 将控制权委托给了 generator1,所以 generator2 会先迭代 generator1 的所有值,然后再继续执行自己的代码。

八、Generator 函数的优缺点

优点 缺点
可以创建自定义迭代器,遍历各种数据结构 学习曲线相对较高,需要理解迭代器和 yield 关键字的概念
可以实现惰性求值,提高性能 代码可读性可能降低,特别是当 Generator 函数嵌套较深时
可以简化异步编程,和 async/await 关键字一起使用 调试难度可能增加,因为 Generator 函数的执行是分段的
可以用来管理复杂的状态,例如游戏的状态机 相比于传统的循环结构,Generator 函数的性能可能会稍差,但这通常不是瓶颈

九、总结

Generator 函数是 JavaScript 中一个非常强大的工具,它可以让我们创建自定义迭代器,实现惰性求值,简化异步编程,管理复杂的状态。虽然学习曲线稍微陡峭一点,但是一旦掌握了它,你就会发现它能极大地提高你的编程效率和代码质量。

总而言之,Generator 函数就像一个百变金刚,可以根据你的需求变幻出各种形态。希望今天的讲座能帮助你更好地理解和使用 Generator 函数。

今天的分享就到这里,感谢大家的收听! 希望各位有所收获,也欢迎大家多多实践,深入理解 Generator 函数的奥妙。 祝大家编程愉快!

发表回复

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