迭代器与生成器

JavaScript 迭代器与生成器

生成器是一种返回迭代器的函数,,使用 yield 操作将函数构造成迭代器。从计算机科学角度上看,生成器是一种类协程或半协程(Semi-coroutine),生成器提供了一种可以通过特定语句或方法来使生成器的执行对象(Execution)暂停,而这语句一般都是 yield。JavaScript 中通过 function 关键字后的星号(*)来表示,函数中会用到新的关键字 yield。在 ES6 中,所有的集合对象(数组、Set 集合及 Map 集合)和字符串都是可迭代对象,可迭代对象都绑定了默认的迭代器。

迭代器 / 生成器第一次出现在 CLU1 语言中,这门语言是由 MIT (美国麻省理工大学)的 Barbara Liskov 教授和她的学生们在 1974 年至 1975 年所设计和开发出来的。这门语言虽然古老,但是却提出了很多如今被广泛使用的编程语言特性,而生成器便是其中的一个。而在 CLU 语言之后,有 Icon 语言 2、Python 语言 3、C# 语言 4 和 Ruby 语言 5 等都受 CLU 语言影响,实现了生成器的特性。在 CLU 语言和 C# 语言中,生成器被称为迭代器(Iterator),而在 Ruby 语言中称为枚举器(Enumerator)。

Generator

普通的函数有一个入口,有一个返回值;当函数被调用时,从入口开始执行,结束时返回相应的返回值。生成器定义的函数,有多个入口和多个返回值;对生成器执行 next() 操作,进行生成器的入口开始执行代码,yield 操作向调用者返回一个值,并将函数挂起;挂起时,函数执行的环境和参数被保存下来;对生成器执行另一个 next() 操作时,参数从挂起状态被重新调用,进入上次挂起的执行环境继续下面的操作,到下一个 yield 操作时重复上面的过程。

yieldnext 在生成器中扮演者非常重要的角色,前者是操作符,后者则是生成器上的属性函数,二者满足如下特性:

  • 生成器的语句会在外部调用 next 函数时执行,即我们可以在生成器之外控制其内部操作的执行过程。
  • 当生成器执行到 yield 操作符时会立即执行 yield 之后的语句并且暂停,语句的值作为上一步 next 函数的返回值,其是形如 {done:false, value:x} 的对象。
  • 继续调用 next 函数会使生成器继续执行,此处 next 函数的参数值会作为整个 yield 语句的值;生成器继续执行直到再次遇到 yield,或是遇到 return/throw 生成器就退出。
  • next 函数的返回值具有三种情况:
    • 如果再次遇到 yieldnext 返回值中的 value 属性是紧接在这条 yield 之后的语句执行之后的返回值;
    • 如果遇到的是 return,那么返回对象 {done:true, value} 则是 return 的返回值;
    • 其他情况下,返回对象 {done:false, value:undefined} ;

我们可以通过简单的例子来验证上述流程描述:

const iter = (function* gen() {
  console.log(`yield ${yield "a" + 0}`);
  console.log(`yield ${yield "b" + 1}`);
  return "c" + 2;
})();

console.log(`next:${iter.next(0).value}`); //输出 next:a0
console.log(`next:${iter.next(1).value}`); //输出 yield 1 next:b1
console.log(`next:${iter.next(2).value}`); //输出 yield 2 next:c2

第一个 next 触发生成器执行到第一个 yield,并立即执行 'a' + 0 = 'a0', a0 作为这次 next 的返回值;第二个带参数为 1next 触发生成器继续执行,此时 第一个 yield 才返回 1,然后执行到 第二个 yield 并立即立即这条 yield 后面的 'b' + 1 = 'b1'b1 作为这次 next的返回;第三个 next 执行以此类推。在其他语言中生成器也经常作为无限数组的生成工具,譬如我们可以使用生成器来实现斐波那契数列:

const f = (function* fibonacci() {
  let [a, b] = [0, 1];

  for (;;) {
    yield a;
    [a, b] = [b, a + b];
  }
})();

//执行三次,得到三个对象,其value值分别是0,1,1
for (let i of Array(3).keys()) {
  console.log(f.next());
}

迭代器 | Iterator

可迭代对象

可迭代对象,都有一个 Symbol.iterator 方法,for-of 循环时,通过调用 colors 数组的 Symbol.iterator 方法来获取默认迭代器的,这一过程是在 JavaScript 引擎背后完成的。

let values = [1, 2, 3];
let iterator = values[Symbol.iterator]();

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}"

在这段代码中,通过 Symbol.iterator 获取了数组 values 的默认迭代器,并用它遍历数组中的元素。在 JavaScript 引擎中执行 for-of 循环语句也是类似的处理过程。

用 Symbol.iterator 属性来检测对象是否为可迭代对象:

function isIterator(object) {
  return typeof object[Symbol.iterator] === "function";
}

console.log(isIterable([1, 2, 3])); // true
console.log(isIterable(new Set())); // true
console.log(isIterable(new Map())); // true
console.log(isIterable("Hello")); // true

当我们在创建对象时,给 Symbol.iterator 属性添加一个生成器,则可以将其变成可迭代对象:

let collection = {
  items: [],
  *[Symbol.iterator]() {
    // 将生成器赋值给对象的Symbol.iterator属性来创建默认的迭代器
    for (let item of this.items) {
      yield item;
    }
  },
};

collection.items.push(1);
collection.items.push(2);
collection.items.push(3);

for (let x of collection) {
  console.log(x);
}

内建迭代器

每个集合类型都有一个默认的迭代器,在 for-of 循环中,如果没有显式指定则使用默认的迭代器。按常规使用习惯,我们很容易猜到,数组和 Set 集合的默认迭代器是 values(),Map 集合的默认迭代器是 entries()。

let colors = ["red", "green", "blue"];
let tracking = new Set([1234, 5678, 9012]);
let data = new Map();

data.set("title", "Understanding ECMAScript 6");
data.set("format", "print");

// 与调用 colors.values() 方法相同
for (let value of colors) {
  console.log(value);
}

// 与调用 tracking.values() 方法相同
for (let num of tracking) {
  console.log(num);
}

// 与调用 data.entries() 方法相同
for (let entry of data) {
  console.log(entry);
}

展开运算符可以操作所有的可迭代对象,并根据默认迭代器来选取要引用的值,从迭代器读取所有值。然后按返回顺序将它们依次插入到数组中。因此如果想将可迭代对象转换为数组,用展开运算符是最简单的方法。

let set = new Set([1, 2, 3, 4, 5]),
  array = [...set];

console.log(array); // [1,2,3,4,5]

迭代器的异常处理

除了给迭代器传递数据外,还可以给它传递错误条件。通过 throw()方法,当迭代器恢复执行时可令其抛出一个错误。

function* createIterator() {
  let first = yield 1;
  let second = yield first + 2; // yield 4 + 2, 然后抛出错误
  yield second + 3; // 永远不会被执行
}

let iterator = createIterator();

console.log(iterator.next()); // "{ value: 1, done: false }"
console.log(iterator.next(4)); // "{ value: 6, done: false }"
console.log(iterator.throw(new Error("Boom"))); // 从生成器中抛出错误

这个示例中,前两个表达式正常求值,而调用 throw()后,在继续执行 let second 求值前,错误就会被抛出并阻止代码继续执行。知道了这一点,就可以在生成器内部通过 try-catch 代码块来捕获这些错误:

function* createIterator() {
  let first = yield 1;
  let second;

  try {
    second = yield first + 2; // yield 4 + 2, 然后抛出错误
  } catch (e) {
    second = 6;
  }
  yield second + 3;
}

let iterator = createIterator();

console.log(iterator.next()); // "{ value: 1, done: false }"
console.log(iterator.next(4)); // "{ value: 6, done: false }"
console.log(iterator.throw(new Error("Boom"))); // "{ value: 9, done: false }"
console.log(iterator.next()); // "{ value: undefined, done: true }"

这里有个有趣的现象:调用 throw()方法后也会像调用 next()方法一样返回一个结果对象。由于在生成器内部捕获了这个错误,因而会继续执行下一条 yield 语句,最终返回数值 9。如此一来,next()和 throw()就像是迭代器的两条指令,调用 next()方法命令迭代器继续执行(可能提供一个值),调用 throw()方法也会命令迭代器继续执行,但同时抛出一个错误,在此之后的执行过程取决于生成器内部的代码。

Links

上一页
下一页