如何在JS中实现生成器函数(迭代协议)
快速掌握如何在 JS 中构建生成器函数以及如何使用 yield 关键字。
阅读全文或在 YouTube 上观看我的编程演示:
简而言之
- 与“手动”实现相比,生成器函数使我们能够编写更简洁的协议实现
iterable。iterator - 生成器函数是通过在关键字后面紧跟一个星号来定义的
function:function* myGenerator() { ... } - 每次调用生成器函数时,它都会返回一个
Generator对象——该对象本身又是被调用生成器函数的一个实例。因此,生成器函数的代码实际上定义了该Generator对象的工作方式。 - 该
Generator对象同时实现了 `iterableand` 和iterator`proto` 协议,因此可以与循环结合使用for ... of ...。这是对象的一个(但并非唯一)主要用途Generator。 - 生成器函数/对象背后的机制可以看作是一种有状态函数。它会记住代码执行中断的位置,并在下次调用时从那里继续执行。
- 关键字
yield是实现此功能的关键。请像使用return关键字一样使用它。它会将给定值返回给调用者,中断生成器函数的执行,并记住需要从哪里继续执行。
基础知识
生成器函数可以看作是创建迭代器对象的一种替代方法,也可以看作是某种有状态的函数。
每次调用一个函数时,它都会从头到尾运行一遍。如果在执行过程中return遇到异常语句,则会将给定的值返回给调用者。如果再次调用同一个函数,它也会再次从头到尾运行一遍。
生成器函数的情况略有不同。它可以被中断,并在后续调用时继续执行。实现这一点的关键在于所谓的yield语句。它的工作方式与普通语句类似return,会将传递给它的值返回给调用者。但它还会记住函数的状态和代码执行的位置。这意味着,如果再次调用生成器函数,它会紧接着yield上次执行的语句继续执行。
因此,要使以下生成器函数从头到尾完整执行,需要调用四次。前三次调用用于获取三个给定值,第四次调用用于终止迭代器(参见 ` next()` 函数的定义)。
function* myGenerator() {
yield 1;
yield 2;
yield 3;
}
let generator = myGenerator();
console.log(generator.next().value); // 1
console.log(generator.next().value); // 2
console.log(generator.next().value); // 3
console.log(generator.next().value); // undefined
iterable/iterator协议和for ... of ...
注意:如果您不熟悉迭代器和/或iterable/协议,观看上一集iterable可能会有所帮助:
JavaScript 提供了两个协议:`Object.get()`iterable和 `Object.get iterator()`。任何实现了 ` iterableObject.get()` 协议的对象(例如数组)都可以在循环中使用,for ... of ...以遍历该对象的内容。`Object.get()`iterable和`Object.get()` 协议iterator紧密相关,因为iterable对象必须提供一个 ` iteratorObject.get()` 对象,该对象通过 `Object.get()` 属性公开一个无参函数Symbol.iterator。虽然听起来很复杂,但实际上只需一行代码即可实现:
const iterator = someIterable[Symbol.iterator]();
但并非总是需要直接操作迭代器,例如,for ... of ...循环本身就隐式地处理可迭代对象。在下面的示例中,someIterable[Symbol.iterator]()循环由运行时调用,并使用该for ... of ...循环返回的迭代器。
for (const value of someIterable) {
console.log(value);
}
用于生成自定义双向链表的生成器函数
完整代码请见
https://github.com/crayon-code/js-doublylinkedlist-generator
此处提供的代码基于上一集中提到的内容。
双向链表是一个节点序列,其中每个节点都知道它的前驱节点和后继节点。因此,每个节点内部都有一个表示实际值的属性(称为value),以及一个表示其前驱节点(称为previous)和后继节点(称为next)的属性。
双向链表的第一个节点称为 n head,最后一个节点称为 n tail。
因此,要编写一个生成器函数,使我们能够从双向链表的开头迭代到结尾,只需要几行代码:
class DoublyLinkedList {
...
// function definitions in a class
// do not require the function
// keyword, so only the asterisk
// is written in front of the
// function identifier
*[Symbol.iterator]() {
// start iterating at the head
let current = this.head;
// current is falsy as soon as
// the last item was passed
// (or the list is empty)
// so the loop would terminate
// (or not even start)
while (current) {
// retrieve the reference
// to the next item as well as
// the current value
const { next, value } = current;
// advance current to the
// (potentially) next item
current = next;
// and (statefully) return the
// current value to the caller
yield value;
// and right after the yield
// statement code execution
// is continued, so the next
// thing that happens is the
// re-evaluation of the
// loop condition
}
}
}
之后的使用就非常简单了:
const dll = new DoublyLinkedList();
...
// Now this implicitly uses
// the generator function behind
// [Symbol.iterator]
for (const item in dll) {
}
反向迭代
此外,编写一个从最后一个元素到第一个元素遍历列表的生成器函数也很容易……
class DoublyLinkedList {
...
*reverse() {
let current = this.tail;
while (current) {
const { value, prev } = current;
current = prev;
yield value;
}
}
}
……而且使用起来也相当简单:
const dll = new DoublyLinkedList();
...
// Note the call to reverse()
for (const item in dll.reverse()) {
}
