JS `Generator` 函数作为迭代器工厂:创建自定义迭代器

好嘞!各位观众老爷们,今天咱们就来聊聊 JavaScript 中那些神出鬼没的 Generator 函数,看看它们是如何化身成为迭代器工厂,打造属于我们自己的迭代器军团的!

开场白:迭代器是个啥玩意儿?

在正式开讲 Generator 之前,咱们先来回顾一下迭代器是个啥。简单来说,迭代器就是个“遍历器”,能让你逐个访问集合中的元素,而不用操心底层的数据结构。想象一下,你有一串葡萄,迭代器就像是那个帮你一颗一颗摘葡萄的小助手。

在 JavaScript 中,迭代器是一个对象,它必须包含一个 next() 方法。这个 next() 方法会返回一个对象,包含两个属性:

  • value:当前迭代到的值。
  • done:一个布尔值,表示迭代是否结束。true 表示迭代完成,false 表示还有更多元素。

举个栗子:

const myArray = [1, 2, 3];

// 手动创建一个迭代器
const myIterator = {
  index: 0,
  next: function() {
    if (this.index < myArray.length) {
      return { value: myArray[this.index++], done: false };
    } else {
      return { value: undefined, done: true };
    }
  }
};

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

这段代码创建了一个手动迭代器,它可以遍历数组 myArray。虽然手动创建迭代器能让你更了解迭代器的本质,但写起来实在太麻烦了!这时候,Generator 函数就该闪亮登场了。

主角登场:Generator 函数的魔法

Generator 函数是一种特殊的函数,它使用 function* 语法定义,并且可以在函数体内使用 yield 关键字。yield 就像一个暂停按钮,可以让函数暂停执行,并将 yield 后面的值作为迭代器的 value 返回。当你再次调用迭代器的 next() 方法时,函数会从上次暂停的地方继续执行。

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

const iterator = myGenerator();

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 }

看到了吗?使用 Generator 函数,我们只需要几行代码就能创建一个迭代器,简直不要太方便!

Generator 函数的特性总结:

特性 描述
语法 使用 function* 定义。
yield 关键字 暂停函数执行,并将 yield 后面的值作为迭代器的 value 返回。
迭代器工厂 调用 Generator 函数会返回一个迭代器对象。
next() 方法 用于继续执行 Generator 函数,并获取下一个 yield 的值。
状态保持 Generator 函数会记住上次 yield 的状态,下次调用 next() 方法时会从上次暂停的地方继续执行。

实战演练:创建自定义迭代器

接下来,咱们来几个实战例子,看看如何使用 Generator 函数创建各种各样的自定义迭代器。

1. 创建一个无限迭代器:斐波那契数列

斐波那契数列是一个经典的数列,它的每一项都是前两项的和。我们可以使用 Generator 函数创建一个无限迭代器,不断生成斐波那契数列的下一项。

function* fibonacci() {
  let a = 0;
  let 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
// ... 无限延续

这个例子展示了 Generator 函数的强大之处,它可以轻松创建无限序列的迭代器。

2. 遍历对象属性:一个更优雅的方式

以前,我们遍历对象属性通常使用 for...in 循环。但是,for...in 循环会遍历对象原型链上的属性,这有时候并不是我们想要的。使用 Generator 函数,我们可以创建一个只遍历对象自身属性的迭代器。

function* objectEntries(obj) {
  for (let key in obj) {
    if (obj.hasOwnProperty(key)) {
      yield [key, obj[key]];
    }
  }
}

const myObj = { a: 1, b: 2, c: 3 };

for (let [key, value] of objectEntries(myObj)) {
  console.log(`${key}: ${value}`);
}
// 输出:
// a: 1
// b: 2
// c: 3

在这个例子中,objectEntries 函数返回一个迭代器,它可以遍历对象 myObj 的所有自身属性。使用 for...of 循环,我们可以轻松地访问每个属性的键和值。

3. 创建一个范围迭代器:指定起始值和结束值

有时候,我们需要一个可以生成指定范围内的数字的迭代器。Generator 函数可以轻松实现这个功能。

function* range(start, end) {
  for (let i = start; i <= end; i++) {
    yield i;
  }
}

const myRange = range(1, 5);

console.log(myRange.next().value); // 1
console.log(myRange.next().value); // 2
console.log(myRange.next().value); // 3
console.log(myRange.next().value); // 4
console.log(myRange.next().value); // 5
console.log(myRange.next().value); // undefined

这个例子创建了一个 range 函数,它可以生成从 startend 的所有整数。

4. 处理异步操作:让异步代码更优雅

Generator 函数还可以与 Promise 结合使用,让异步代码更加优雅。通过 yield 一个 Promise 对象,我们可以暂停 Generator 函数的执行,直到 Promise 对象 resolve。

function fetchData(url) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve(`Data from ${url}`);
    }, 1000);
  });
}

function* myAsyncGenerator() {
  const data1 = yield fetchData('url1');
  console.log(data1);
  const data2 = yield fetchData('url2');
  console.log(data2);
}

function run(generator) {
  const iterator = generator();

  function handleNext(value) {
    const next = iterator.next(value);
    if (next.done) {
      return;
    }
    if (next.value instanceof Promise) {
      next.value.then(handleNext);
    } else {
      handleNext(next.value);
    }
  }

  handleNext();
}

run(myAsyncGenerator);
// 输出:
// Data from url1
// Data from url2

在这个例子中,myAsyncGenerator 函数使用 yield 关键字暂停执行,等待 fetchData 函数返回的 Promise 对象 resolve。run 函数负责驱动 Generator 函数的执行,并处理 Promise 对象。

5. 更复杂的例子:树的深度优先遍历

假设我们有一个树结构,我们想要使用迭代器来实现树的深度优先遍历。

class TreeNode {
  constructor(value) {
    this.value = value;
    this.children = [];
  }

  addChild(node) {
    this.children.push(node);
    return this;
  }
}

function* depthFirstTraversal(root) {
  yield root.value;
  for (let child of root.children) {
    yield* depthFirstTraversal(child); // 使用 yield* 递归调用
  }
}

const root = new TreeNode('A');
root.addChild(new TreeNode('B'))
    .addChild(new TreeNode('C')
                .addChild(new TreeNode('D'))
                .addChild(new TreeNode('E')));
root.addChild(new TreeNode('F'));

for (let value of depthFirstTraversal(root)) {
  console.log(value);
}
// 输出:
// A
// B
// C
// D
// E
// F

这个例子展示了如何使用 yield* 关键字来委托给另一个迭代器。yield* depthFirstTraversal(child) 会将 child 树的深度优先遍历委托给另一个 depthFirstTraversal 函数,从而实现整个树的深度优先遍历。

yield* 的妙用

在上面的例子中,我们看到了 yield* 关键字的用法。yield* 允许我们将一个迭代器委托给另一个迭代器。它的作用是将另一个迭代器产生的所有值都 yield 出来,就像将另一个迭代器的代码直接插入到当前迭代器中一样。

*`yield` 的作用:**

  • 委托迭代: 将一个迭代器的控制权交给另一个迭代器。
  • 代码复用: 避免重复编写迭代逻辑。
  • 递归迭代: 在递归函数中使用,可以方便地遍历树形结构等复杂数据结构。

总结:Generator 函数,迭代器的瑞士军刀

总而言之,Generator 函数是 JavaScript 中一个非常强大的工具,它可以帮助我们轻松创建各种各样的自定义迭代器。无论是简单的范围迭代器,还是复杂的异步迭代器,Generator 函数都能胜任。掌握了 Generator 函数,你就能像拥有了一把迭代器的瑞士军刀,在处理集合数据时游刃有余。

希望今天的讲座能帮助大家更好地理解 Generator 函数,并在实际开发中灵活运用。下次再见!

发表回复

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