本章内容
- 理解迭代
- 迭代器模式
- 生成器
在软件开发领域,“迭代”的意思是按照顺序反复多次执行一段程序,通常会有明确的终止条件。
ECMAScript 6 规范新增了两个高级特性:迭代器和生成器。使用这两个特性,能够更清晰、高效、方便地实现迭代。
- 循环是迭代机制的基础,这是因为它可以指定迭代的次数,以及每次迭代要执行什么操作。
- 每次循环都会在下一次迭代开始之前完成,而每次迭代的顺序都是事先定义好的。
- 迭代会在一个有序集合上进行。
- (“有序”可以理解为集合中所有项都可以按照既定的顺序被遍历到,特别是开始和结束项有明确的定义。
- 通过这种循环来执行例程并不理想:
- 迭代之前需要事先知道如何使用数据结构。
- 遍历顺序并不是数据结构固有的。
很多语言都通过原生语言结构解决了这个问题,开发者无须事先知道如何迭代就能实现迭代操作。这个解决方案就是迭代器模式。
-
迭代器模式(特别是在 ECMAScript 这个语境下)描述了一个方案,即可以把有些结构称为“可迭代对象”(iterable),因为它们实现了正式的 Iterable 接口,而且可以通过迭代器 Iterator 消费。
-
可迭代对象是一种抽象的说法。基本上,可以把可迭代对象理解成数组或集合这样的集合类型的对象。它们包含的元素都是有限的,而且都具有无歧义的遍历顺序。
-
不过,可迭代对象不一定是集合对象,也可以是仅仅具有类似数组行为的其他数据结构
-
任何实现 Iterable 接口的数据结构都可以被实现 Iterator 接口的结构“消费”(consume)。
-
迭代器(iterator)是按需创建的一次性对象。每个迭代器都会关联一个可迭代对象,而迭代器会暴露迭代其关联可迭代对象的 API。
-
迭代器无须了解与其关联的可迭代对象的结构,只需要知道如何取得连续的值。
迭代器可以为各种不同的数据结构提供统一的访问机制。任何数据结构只要部署 Iterator 接口,就可以完成遍历操作。
迭代器的作用:
- 为各种数据结构,提供一个统一的、简便的访问接口。
- 使得数据结构的成员能够按某种次序排列。
- ECMAScript 6 新增了 for…of 循环语句,用于遍历迭代器。
实现 Iterable 接口(可迭代协议)要求同时具备两种能力:支持迭代的自我识别能力和创建实现 Iterator 接口的对象的能力。
- 要成为可迭代对象, 一个对象必须实现 @@iterator 方法。这意味着对象(或者它原型链上的某个对象)必须有一个键为 @@iterator 的属性,可通过常量 Symbol.iterator 访问该属性。
- 这个默认迭代器属性(@@iterator 的属性)必须引用一个迭代器工厂函数,调用这个工厂函数必须返回一个新迭代器。
- 当一个对象需要被迭代的时候(比如被置入一个 for...of 循环时),首先,会不带参数调用它的 @@iterator 方法,然后使用此方法返回的迭代器获得要迭代的值。
- 如果对象原型链上的父类实现了 Iterable 接口,那这个对象也就实现了这个接口。
简单说来,一个数据结构只要具有 Symbol.iterator 属性,就可以认为是“可迭代的”。
为了变成可迭代对象, 一个对象必须实现(或者它原型链的某个对象)必须有一个名字是 Symbol.iterator 的属性。
let num = 1;
let obj = {};
// 这两种类型没有实现迭代器工厂函数
console.log(num[Symbol.iterator]); // undefined
console.log(obj[Symbol.iterator]); // undefined
let str = "abc";
let arr = ["a", "b", "c"];
let map = new Map().set("a", 1).set("b", 2).set("c", 3);
let set = new Set().add("a").add("b").add("c");
// 这些类型都实现了迭代器工厂函数
console.log(str[Symbol.iterator]); // [Function: [Symbol.iterator]]
console.log(arr[Symbol.iterator]); // [Function: values]
console.log(map[Symbol.iterator]); // [Function: entries]
console.log(set[Symbol.iterator]); // [Function: values]
// 调用这个工厂函数会生成一个迭代器
console.log(str[Symbol.iterator]()); // Object [String Iterator] {}
console.log(arr[Symbol.iterator]()); // Object [Array Iterator] {}
console.log(map[Symbol.iterator]()); // [Map Entries] { [ 'a', 1 ], [ 'b', 2 ], [ 'c', 3 ] }
console.log(set[Symbol.iterator]()); // [Set Iterator] { 'a', 'b', 'c' }
接收可迭代对象的原生语言特性包括:
- for-of 循环
- 数组解构
- 扩展操作符
- Array.from()
- 创建集合
- 创建映射
- Promise.all() 接收由期约组成的可迭代对象
- Promise.race() 接收由期约组成的可迭代对象
- yield* 操作符,在生成器中使用
简单记忆:可迭代对象即具有 Symbol.iterator 属性的数据结构。
迭代器协议定义了产生一系列值(无论是有限个还是无限个)的标准方式。当值为有限个时,所有的值都被迭代完毕后,则会返回一个默认返回值。
只有实现了一个拥有以下语义(semantic)的 next() 方法,一个对象才能成为迭代器:
- next() 方法返回的迭代器对象 IteratorResult 包含两个属性: done 和 value 。
- done 是一个布尔值,表示是否还可以再次调用 next() 取得下一个值;
- 如果迭代器可以产生序列中的下一个值,则为 false。(这等价于没有指定 done 这个属性。)
- 如果迭代器已将序列迭代完毕,则为 true。这种情况下,value 是可选的,如果它依然存在,即为迭代结束之后的默认返回值。
- value 包含可迭代对象的下一个值( done 为 false ),或者 undefined ( done 为 true )。 done: true 状态称为“耗尽”。
- 迭代器返回的任何 JavaScript 值。done 为 true 时可省略。
- next() 方法必须返回一个对象,该对象应当有两个属性: done 和 value,如果返回了一个非对象值(比如 false 或 undefined),则会抛出一个 TypeError 异常("iterator.next() returned a non-object value")。
// 可迭代对象
let arr = ["foo", "bar"];
// 迭代器工厂函数
console.log(arr[Symbol.iterator]); // [Function: values]
// 迭代器
let iter = arr[Symbol.iterator]();
console.log(iter); // Object [Array Iterator] {}
// 执行迭代
console.log(iter.next()); // { value: 'foo', done: false }
console.log(iter.next()); // { value: 'bar', done: false }
console.log(iter.next()); // { value: undefined, done: true }
简单记忆:迭代器即实现了特定 next()方法的对象。
很少会只实现迭代器协议,而不实现可迭代协议。
迭代器维护着一个指向可迭代对象的引用,因此迭代器会阻止垃圾回收程序回收可迭代对象。
与 Iterable 接口类似,任何实现 Iterator 接口的对象都可以作为迭代器使用。
class Counter {
constructor(limit) {
this.limit = limit;
}
[Symbol.iterator]() {
let count = 1,
limit = this.limit;
return {
next() {
if (count <= limit) {
return { done: false, value: count++ };
} else {
return { done: true, value: undefined };
}
},
};
}
}
let counter = new Counter(3);
for (let i of counter) {
console.log(i);
}
// 1
// 2
// 3
可选的 return() 方法用于指定在迭代器提前关闭时执行的逻辑。 执行迭代的结构在想让迭代器知道它不想遍历到可迭代对象耗尽时,就可以“关闭”迭代器。可能的情况包括:
- for-of 循环通过 break 、 continue 、 return 或 throw 提前退出;
- 解构操作并未消费所有值。
return() 方法必须返回一个有效的 IteratorResult 对象。简单情况下,可以只返回 { done: true } 。
class Counter {
constructor(limit) {
this.limit = limit;
}
[Symbol.iterator]() {
let count = 1,
limit = this.limit;
return {
next() {
if (count <= limit) {
return { done: false, value: count++ };
} else {
return { done: true };
}
},
return() {
console.log("Exiting early");
return { done: true };
},
};
}
}
如果迭代器没有关闭,则还可以继续从上次离开的地方继续迭代。
因为 return() 方法是可选的,所以并非所有迭代器都是可关闭的。
- 要知道某个迭代器是否可关闭,可以测试这个迭代器实例的 return 属性是不是函数对象。
- 不过,仅仅给一个不可关闭的迭代器增加这个方法并不能让它变成可关闭的。
- 这是因为调用 return() 不会强制迭代器进入关闭状态。即便如此,return() 方法还是会被调用。
生成器是 ECMAScript 6 新增的一个极为灵活的结构,拥有在一个函数块内暂停和恢复代码执行的能力。
生成器的形式是一个函数,函数名称前面加一个星号( * )表示它是一个生成器。只要是可以定义函数的地方,就可以定义生成器。
- 箭头函数不能用来定义生成器函数。
// 生成器函数声明
function* generatorFn() {}
// 生成器函数表达式
let generatorFn = function* () {};
// 作为对象字面量方法的生成器函数
let foo = {
*generatorFn() {},
};
// 作为类实例方法的生成器函数
class Foo {
*generatorFn() {}
}
// 作为类静态方法的生成器函数
class Bar {
static *generatorFn() {}
}
- 调用生成器函数会产生一个生成器对象。
- 生成器对象一开始处于暂停执行(suspended)的状态。
- 与迭代器相似,生成器对象也实现了 Iterator 接口,因此具有 next() 方法。调用这个方法会让生成器开始或恢复执行。
- next() 方法的返回值类似于迭代器,有一个 done 属性和一个 value 属性。
- 函数体为空的生成器函数中间不会停留,调用一次 next() 就会让生成器到达 done: true 状态。
- value 属性是生成器函数的返回值,默认值为 undefined ,可以通过生成器函数的返回值指定
- next() 方法的返回值类似于迭代器,有一个 done 属性和一个 value 属性。
- 生成器函数只会在初次调用 next() 方法后开始执行。
- 生成器对象实现了 Iterable 接口,它们默认的迭代器是自引用的。
function* generatorFn() {
console.log("foobar");
}
console.log(generatorFn); // [GeneratorFunction: generatorFn]
console.log(generatorFn()[Symbol.iterator]); // [Function: [Symbol.iterator]]
console.log(generatorFn()); // Object [Generator] {} ->浏览器中显示: generatorFn {<suspended>}
console.log(generatorFn()[Symbol.iterator]()); // Object [Generator] {} ->浏览器中显示:generatorFn {<suspended>}
const g = generatorFn();
console.log(g.next());
// 依次输出:
// foobar
// { value: undefined, done: true }
console.log(g === g[Symbol.iterator]()); // true
yield 关键字可以让生成器停止和开始执行,也是生成器最有用的地方。 生成器函数在遇到 yield 关键字之前会正常执行。 遇到这个关键字后,执行会停止,函数作用域的状态会被保留。 停止执行的生成器函数只能通过在生成器对象上调用 next() 方法来恢复执行。
生成器函数内部的执行流程会针对每个生成器对象区分作用域。在一个生成器对象上调用 next()不会影响其他生成器。 yield 关键字只能在生成器函数内部使用,用在其他地方会抛出错误。
- yield 关键字必须直接位于生成器函数定义中,出现在嵌套的非生成器函数中会抛出语法错误。
function* generatorFn() {
yield "foo";
yield "bar";
return "baz";
}
let generatorObject1 = generatorFn();
let generatorObject2 = generatorFn();
console.log(generatorObject1.next()); // { value: 'foo', done: false }
console.log(generatorObject2.next()); // { value: 'foo', done: false }
console.log(generatorObject2.next()); // { value: 'bar', done: false }
console.log(generatorObject1.next()); // { value: 'bar', done: false }
console.log(generatorObject1.next()); // { value: 'baz', done: true }
// yield 关键字的位置
// 有效
function* validGeneratorFn() {
yield;
}
// 无效
function* invalidGeneratorFnA() {
function a() {
yield;
}
}
// 无效
function* invalidGeneratorFnB() {
const b = () => {
yield;
};
}
// 无效
function* invalidGeneratorFnC() {
(() => {
yield;
})();
}
相关操作:
-
- 生成器对象作为可迭代对象
- 在需要自定义迭代对象时,这样使用生成器对象会特别有用。
// 定义一个可迭代对象,产生一个迭代器,这个迭代器会执行指定的次数。
function* nTimes(n) {
while (n--) {
yield;
}
}
for (let _ of nTimes(3)) {
console.log("foo");
}
// foo
// foo
// foo
-
- 使用 yield 实现输入和输出
- yield 关键字还可以作为函数的中间参数使用。
- yield 关键字可以同时用于输入和输出。
- yield 关键字并非只能使用一次。
-
- 产生可迭代对象
- 可以使用星号增强 yield 的行为,让它能够迭代一个可迭代对象,从而一次产出一个值。
- 与生成器函数的星号类似, yield 星号两侧的空格不影响其行为
- 因为 yield* 实际上只是将一个可迭代对象序列化为一连串可以单独产出的值,所以这跟把 yield 放到一个循环里没什么不同。
- yield* 的值是关联迭代器返回 done: true 时的 value 属性。
- 对于普通迭代器来说,这个值是 undefined。
- 对于生成器函数产生的迭代器来说,这个值就是生成器函数返回的值。
-
- 使用 yield*实现递归算法
function* nTimes(n) {
if (n > 0) {
yield* nTimes(n - 1);
yield n - 1;
}
}
for (const x of nTimes(3)) {
console.log(x);
}
// 0
// 1
// 2
用于可迭代对象的语法:例如 for-of 循环,展开语法,yield* 和 解构赋值。
for (let value of ["a", "b", "c"]) {
console.log(value);
}
// "a"
// "b"
// "c"
console.log([..."abc"]); // ["a", "b", "c"]
function* gen() {
yield* ["a", "b", "c"];
}
console.log(gen().next()); // { value: "a", done: false }
[a, b, c] = new Set(["a", "b", "c"]);
console.log(a); // "a"
因为生成器对象实现了 Iterable 接口,而且生成器函数和默认迭代器被调用之后都产生迭代器,所以生成器格外适合作为默认迭代器。
class Foo {
constructor() {
this.values = [1, 2, 3];
}
*[Symbol.iterator]() {
yield* this.values;
}
}
const f = new Foo();
// 这里, for-of 循环调用了默认迭代器(它恰好又是一个生成器函数)并产生了一个生成器对象。
// 这个生成器对象是可迭代的,所以完全可以在迭代中使用。
for (const x of f) {
console.log(x);
}
// 1
// 2
// 3
- 可选的 return() 方法
- return() 方法会强制生成器进入关闭状态。提供给 return() 方法的值,就是终止迭代器对象的值。
- 与迭代器不同,所有生成器对象都有 return() 方法,只要通过它进入关闭状态,就无法恢复了。
- 后续调用 next() 会显示 done: true 状态,而提供的任何返回值都不会被存储或传播。
- for-of 循环等内置语言结构会忽略状态为 done: true 的 IteratorObject 内部返回的值。
- throw()
- throw() 方法会在暂停的时候将一个提供的错误注入到生成器对象中。如果错误未被处理,生成器就会关闭。
- 假如生成器函数内部处理了这个错误,那么生成器就不会关闭,而且还可以恢复执行。错误处理会跳过对应的 yield。
function* generatorFn() {
for (const x of [1, 2, 3]) {
try {
yield x;
} catch (e) {}
}
}
const g = generatorFn();
console.log(g.next()); // { value: 1, done: false }
g.throw("foo");
console.log(g.next()); // { value: 3, done: false }
生成器概述:
- 虽然自定义的迭代器是一个有用的工具,但由于需要显式地维护其内部状态,因此需要谨慎地创建。
- 生成器函数提供了一个强大的选择:它允许你定义一个包含自有迭代算法的函数, 同时它可以自动维护自己的状态。
- 生成器函数使用 function*语法编写。
- 最初调用时,生成器函数不执行任何代码,而是返回一种称为 Generator 的迭代器。
- 通过调用生成器的下一个方法消耗值时,Generator 函数将执行,直到遇到 yield 关键字。
- 可以根据需要多次调用该函数,并且每次都返回一个新的 Generator,但每个 Generator 只能迭代一次。
迭代是一种所有编程语言中都可以看到的模式。ECMAScript 6 正式支持迭代模式并引入了两个新的语言特性:迭代器和生成器。
迭代器
- 迭代器是一个可以由任意对象实现的接口,支持连续获取对象产出的每一个值。
- 任何实现 Iterable 接口的对象都有一个 Symbol.iterator 属性,这个属性引用默认迭代器。
- 默认迭代器就像一个迭代器工厂,也就是一个函数,调用之后会产生一个实现 Iterator 接口的对象。
- 迭代器必须通过连续调用 next() 方法才能连续取得值,这个方法返回一个 IteratorObject 。
- 这个对象包含一个 done 属性和一个 value 属性。前者是一个布尔值,表示是否还有更多值可以访问;后者包含迭代器返回的当前值。
- 这个接口可以通过手动反复调用 next() 方法来消费,也可以通过原生消费者,比如 for-of 循环来自动消费。
生成器
- 生成器是一种特殊的函数,调用之后会返回一个生成器对象。
- 生成器对象实现了 Iterable 接口,因此可用在任何消费可迭代对象的地方。
- 生成器的独特之处在于支持 yield 关键字,这个关键字能够暂停执行生成器函数。
- 使用 yield 关键字还可以通过 next() 方法接收输入和产生输出。
- 在加上星号之后, yield 关键字可以将跟在它后面的可迭代对象序列化为一连串值。
2022-02-23 说明:关于迭代器和生成器(Iterator and Generator)这部分的内容,个人从《JavaScript 高级程序设计(第 4 版)》和 MDN迭代器和生成器章节混合在一起的这部分整理感觉很混乱,没有把握到重点,后续会单独纯个人理解层面,简单总结。