JS `yield*` 表达式:委托给另一个 `Generator` 或可迭代对象

好嘞,各位听众朋友们,今天咱们来聊聊 JavaScript 里一个有点意思的小家伙:yield* 表达式。这货啊,就像个中间人,专门负责把活儿甩给别人干,自己落得清闲。

*一、`yield` 是个啥?**

简单来说,yield* 是一个表达式,只能在 Generator 函数里面用。它的作用是把控制权委托给另一个 Generator 或者任何可迭代对象(比如数组、字符串、Map、Set 等等)。

你可以把它想象成一个包工头,自己不直接干活,而是把工程分包给其他队伍。yield* 后面跟着的就是那个被委托的队伍。

*二、为什么要用 `yield`?**

可能有人会问了,直接用 yield 不行吗?为什么要多此一举搞个 yield* 出来?

答案是:为了更优雅地组织你的 Generator 函数,提高代码的可读性和可维护性。

想象一下,如果你的 Generator 函数特别复杂,里面有很多小的任务,每个任务都可以用一个独立的 Generator 函数来完成。这时候,你就可以用 yield* 把这些小的 Generator 函数串联起来,就像流水线一样,一个接一个地执行。

*三、`yield` 的基本用法**

咱们先来个最简单的例子:

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

function* generator2() {
  yield* generator1(); // 委托给 generator1
  yield 3;
}

const iterator = generator2();

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

在这个例子里,generator2yield* generator1() 把控制权委托给了 generator1。当 generator2 被调用时,它会先执行 generator1,把 generator1 里面的 yield 值依次返回,直到 generator1 执行完毕,然后才会继续执行 generator2 自身的 yield 3

*四、`yield` 委托给可迭代对象**

yield* 不仅仅可以委托给 Generator 函数,还可以委托给任何可迭代对象。比如:

function* generator() {
  yield* [1, 2, 3]; // 委托给数组
  yield* "abc";     // 委托给字符串
  yield 4;
}

const iterator = generator();

console.log(iterator.next()); // { value: 1, done: false }
console.log(iterator.next()); // { value: 2, done: false }
console.log(iterator.next()); // { value: 3, done: false }
console.log(iterator.next()); // { value: 'a', done: false }
console.log(iterator.next()); // { value: 'b', done: false }
console.log(iterator.next()); // { value: 'c', done: false }
console.log(iterator.next()); // { value: 4, done: false }
console.log(iterator.next()); // { value: undefined, done: true }

在这个例子里,generator 分别委托给了一个数组和一个字符串。yield* 会把数组和字符串里面的每个元素依次 yield 出来。

*五、`yield` 的返回值**

yield* 表达式的返回值是被委托的 Generator 函数或者可迭代对象的 return 值。

啥意思呢?看个例子:

function* generator1() {
  yield 1;
  yield 2;
  return "generator1 done";
}

function* generator2() {
  const result = yield* generator1(); // 委托给 generator1
  console.log("result:", result); // result: generator1 done
  yield 3;
}

const iterator = generator2();

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

在这个例子里,generator1return 值是 "generator1 done"。这个值会被 yield* generator1() 表达式接收到,并赋值给 result 变量。

注意: 如果被委托的是一个可迭代对象(比如数组),那么 yield* 的返回值是 undefined,因为可迭代对象没有 return 值。

*六、`yieldreturnthrow`**

yield* 表达式在处理 returnthrow 的时候,也有一些需要注意的地方。

  • return

    如果被委托的 Generator 函数执行到 return 语句,那么 yield* 表达式会立即完成,并将 return 值返回。

  • throw

    如果在被委托的 Generator 函数里面抛出了一个异常,那么这个异常会被传递到 yield* 表达式,并由调用 next() 方法的代码来处理。

    举个例子:

    function* generator1() {
      yield 1;
      throw new Error("Something went wrong!");
    }
    
    function* generator2() {
      try {
        yield* generator1();
        yield 2; // 这行代码不会执行
      } catch (error) {
        console.error("Caught an error:", error.message); // Caught an error: Something went wrong!
      }
      yield 3;
    }
    
    const iterator = generator2();
    
    console.log(iterator.next()); // { value: 1, done: false }
    console.log(iterator.next()); // Caught an error: Something went wrong!
    // { value: 3, done: false }
    console.log(iterator.next()); // { value: undefined, done: true }

    在这个例子里,generator1 抛出了一个异常。这个异常被 generator2try...catch 块捕获,并进行了处理。

*七、`yield` 的实际应用场景**

yield* 在实际开发中有很多应用场景,比如:

  • 异步任务的串行执行:

    可以使用 yield* 把多个异步任务串联起来,按照顺序执行。

    function delay(ms) {
      return new Promise(resolve => setTimeout(resolve, ms));
    }
    
    async function* task1() {
      console.log("Task 1 started");
      await delay(1000);
      console.log("Task 1 finished");
      yield "Result from task 1";
      return "Task 1 final result";
    }
    
    async function* task2() {
      console.log("Task 2 started");
      await delay(500);
      console.log("Task 2 finished");
      yield "Result from task 2";
      return "Task 2 final result";
    }
    
    async function* mainTask() {
      console.log("Main task started");
      const result1 = yield* task1();
      console.log("Task 1 returned:", result1); // "Task 1 final result"
      const result2 = yield* task2();
      console.log("Task 2 returned:", result2); // "Task 2 final result"
      console.log("Main task finished");
    }
    
    async function runMainTask() {
      const iterator = mainTask();
      let result = await iterator.next();
      while (!result.done) {
        console.log("Yielded Value:", result.value);
        result = await iterator.next();
      }
      console.log("Final Result:", result.value);
    }
    
    runMainTask();
    // Output:
    // Main task started
    // Task 1 started
    // Task 1 finished
    // Yielded Value: Result from task 1
    // Task 1 returned: Task 1 final result
    // Task 2 started
    // Task 2 finished
    // Yielded Value: Result from task 2
    // Task 2 returned: Task 2 final result
    // Main task finished
    // Final Result: undefined
  • 递归遍历树形结构:

    可以使用 yield* 递归地遍历树形结构,把每个节点的值 yield 出来。

    function* traverseTree(node) {
      yield node.value;
      if (node.children) {
        for (const child of node.children) {
          yield* traverseTree(child); // 递归调用
        }
      }
    }
    
    const tree = {
      value: "A",
      children: [
        { value: "B", children: [{ value: "D" }, { value: "E" }] },
        { value: "C", children: [{ value: "F" }] },
      ],
    };
    
    const iterator = traverseTree(tree);
    
    console.log(iterator.next()); // { value: 'A', done: false }
    console.log(iterator.next()); // { value: 'B', done: false }
    console.log(iterator.next()); // { value: 'D', done: false }
    console.log(iterator.next()); // { value: 'E', done: false }
    console.log(iterator.next()); // { value: 'C', done: false }
    console.log(iterator.next()); // { value: 'F', done: false }
    console.log(iterator.next()); // { value: undefined, done: true }
  • 状态机的实现:

    可以使用 yield* 把状态机的不同状态用不同的 Generator 函数来表示,然后用 yield* 在这些状态之间进行切换。

    function* stateA() {
      console.log("Entering state A");
      yield "State A action 1";
      yield "State A action 2";
      return "State A completed";
    }
    
    function* stateB() {
      console.log("Entering state B");
      yield "State B action 1";
      yield "State B action 2";
      return "State B completed";
    }
    
    function* stateMachine() {
      console.log("State Machine Started");
      const resultA = yield* stateA();
      console.log("State A result:", resultA);
      const resultB = yield* stateB();
      console.log("State B result:", resultB);
      console.log("State Machine Finished");
    }
    
    const iterator = stateMachine();
    let result = iterator.next();
    while (!result.done) {
      console.log("Yielded Value:", result.value);
      result = iterator.next();
    }
    console.log("Final Result:", result.value);
    
    // Output:
    // State Machine Started
    // Entering state A
    // Yielded Value: State A action 1
    // Yielded Value: State A action 2
    // State A result: State A completed
    // Entering state B
    // Yielded Value: State B action 1
    // Yielded Value: State B action 2
    // State B result: State B completed
    // State Machine Finished
    // Final Result: undefined

*八、`yield` 的注意事项**

  • yield* 只能在 Generator 函数里面使用。
  • yield* 后面必须跟着一个 Generator 函数或者可迭代对象。
  • yield* 表达式的返回值是被委托的 Generator 函数或者可迭代对象的 return 值。
  • yield* 可以用来简化代码,提高可读性和可维护性。

*九、`yieldyield` 的区别**

特性 yield yield*
作用 暂停 Generator 函数的执行,并返回一个值。 将控制权委托给另一个 Generator 函数或可迭代对象。
使用场景 返回单个值。 串联多个 Generator 函数或可迭代对象,执行复杂任务。
返回值 next() 方法传入的参数。 被委托的 Generator 函数或可迭代对象的 return 值。
是否必须在Generator中使用

十、总结

yield* 是一个非常强大的表达式,它可以让你更优雅地组织你的 Generator 函数,提高代码的可读性和可维护性。虽然它看起来有点复杂,但只要你理解了它的基本原理,就可以在实际开发中灵活运用。

希望今天的讲座能让你对 yield* 有更深入的了解。记住,编程就像练武,光说不练假把式,多写代码才是王道!

最后,祝大家编程愉快,早日成为编程高手!下课!

发表回复

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