迭代器与生成器
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 操作时重复上面的过程。
yield
与 next
在生成器中扮演者非常重要的角色,前者是操作符,后者则是生成器上的属性函数,二者满足如下特性:
- 生成器的语句会在外部调用
next
函数时执行,即我们可以在生成器之外控制其内部操作的执行过程。 - 当生成器执行到
yield
操作符时会立即执行yield
之后的语句并且暂停,语句的值作为上一步next
函数的返回值,其是形如{done:false, value:x}
的对象。 - 继续调用
next
函数会使生成器继续执行,此处next
函数的参数值会作为整个yield
语句的值;生成器继续执行直到再次遇到yield
,或是遇到return
/throw
生成器就退出。 next
函数的返回值具有三种情况:- 如果再次遇到
yield
,next
返回值中的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
的返回值;第二个带参数为 1
的 next
触发生成器继续执行,此时 第一个 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()方法也会命令迭代器继续执行,但同时抛出一个错误,在此之后的执行过程取决于生成器内部的代码。