迭代的英文 「iteration」 源自拉丁文 itero,意思是「重复」或「再来」​。在软件开发领域,​「迭代」的意思是按照顺序反复多次执行一段程序,通常会有明确的终止条件。

在 ECMAScript 较早的版本中,执行迭代必须使用循环或其他辅助结构。随着代码量增加,代码会变得越发混乱。很多语言都通过原生语言结构解决了这个问题,开发者无须事先知道如何迭代就能实现迭代操作。这个解决方案就是迭代器模式。Python、Java、C++,还有其他很多语言都对这个模式提供了完备的支持。

ECMAScript6 规范新增了两个高级特性:迭代器和生成器。增加这两个特性目的便是为了能够更清晰、高效、方便地实现迭代。

迭代器和生成器的核心都是——「迭代」

本文将会采用 JavaScript 作为示例语言,来介绍迭代器和生成器的概念和用法。

最基础的迭代

在 JavaScript 中,计数循环就是一种最简单的迭代:

for(let i = 0; i < 10; i++) {
    console.log(i)
}

循环是迭代机制的基础,这是因为它可以指定迭代的次数,以及每次迭代要执行什么操作。

每次循环都会在下一次迭代开始之前完成,而每次迭代的顺序都是事先定义好的。迭代会在一个有序集合上进行。​数组是JavaScript中有序集合的最典型例子。

let collection = ['one', 'two', 'three'];
for (let index = 0; index < collection.length; ++index) {      
    console.log(collection[index]);    
}

因为数组有已知的长度,且数组每一项都可以通过索引获取,所以整个数组可以通过递增索引来遍历。由于如下原因,通过这种循环来执行例程并不理想。

ES5新增了Array.prototype.forEach()方法,向通用迭代需求迈进了一步,但仍然不够理想。

collection.forEach((item) => console.log(item));

这个方法解决了单独记录索引和通过数组对象取得值的问题。不过,没有办法标识迭代何时终止。因此这个方法只适用于数组,而且回调结构也比较笨拙。

迭代器

迭代器模式提供了一种解决方案,即可以把有些结构称为「可迭代对象」​,因为它们实现了正式的 Iterable 接口,而且可以通过迭代器 Iterator 消费

此处的「消费」是指使用迭代器提供的 next() 方法,每次调用都会返回数据结构中的下一个值。可以简单理解为对 Iterable 返回的结果进行使用。

可迭代对象是一种抽象的说法。基本上,可以把可迭代对象理解成数组或集合这样的集合类型的对象。它们包含的元素都是有限的,而且都具有无歧义的遍历顺序。

不过,可迭代对象不一定是集合对象,也可以是仅仅具有类似数组行为的其他数据结构,前面提到的计数循环。该循环中生成的值是暂时性的,但循环本身是在执行迭代。计数循环和数组都具有可迭代对象的行为。

任何实现 Iterable 接口的数据结构都可以被实现 Iterator 接口的结构「消费」​。
iterator 是按需创建的一次性对象。每个迭代器都会关联一个可迭代对象,而迭代器会暴露迭代其关联可迭代对象的 API。迭代器无须了解与其关联的可迭代对象的结构,只需要知道如何取得连续的值。这种概念上的分离正是 Iterable 和 Iterator 的强大之处。

可迭代协议

实现 Iterable 接口——可迭代协议——要求同时具备两种能力:支持迭代的自我识别能力和创建实现 Iterator 接口的对象的能力。在 ECMAScript 中,这意味着必须暴露一个属性作为「默认迭代器」​,而且这个属性必须使用特殊的 Symbol.iterator 作为键。这个默认迭代器属性必须引用一个迭代器工厂函数,调用这个工厂函数必须返回一个新迭代器。

很多内置类型都实现了Iterable接口:字符串、数组、映射、集合、arguments对象…

let set = new Set().add('one').add('two').add('three');
console.log(set[Symbol.iterator]);      // f values() { [native code] }
console.log(set[Symbol.iterator]());    // SetIterator {}

for(let item of set) {
    console.log(item); 
}

实际写代码过程中,不需要显式调用这个工厂函数来生成迭代器。实现可迭代协议的所有类型都会自动兼容接收可迭代对象的任何语言特性。接收可迭代对象的原生语言特性包括,如:for-of 循环、数组解构、扩展操作符、Array.from()Promise.all()

迭代器协议

迭代器是一种一次性使用的对象,用于迭代与其关联的可迭代对象。迭代器 API 使用 next() 方法在可迭代对象中遍历数据。每次成功调用 next() ,都会返回一个 IteratorResult 对象,其中包含迭代器返回的下一个值。若不调用 next() ,则无法知道迭代器的当前位置。

next() 方法返回的迭代器对象 IteratorResult 包含两个属性:done 和 value。done 是一个布尔值,表示是否还可以再次调用 next() 取得下一个值;value 包含可迭代对象的下一个值(done:false)​,或者 undefined(done:true​)。done 为 true 时,就表示状态称为「耗尽」​。

// 可迭代对象
let arr = ['foo', 'bar']; 

// 获取迭代器
let iter = arr[Symbol.iterator](); 
  
// 「消费」迭代器 
console.log(iter.next()); // { done: false, value: 'foo' } 
console.log(iter.next()); // { done: false, value: 'bar' } 
console.log(iter.next()); // { done: true,  value: undefined }

迭代器并不与可迭代对象某个时刻的快照绑定,而仅仅是使用游标来记录遍历可迭代对象的历程。如果可迭代对象在迭代期间被修改了,那么迭代器也会反映相应的变化。

let arr = ['foo', 'baz'];

let iter = arr[Symbol.iterator]();

console.log(iter.next()); // { done: false, value: 'foo' } 

// 在数组中间插入值
arr.splice(1, 0, 'bar'); 
console.log(iter.next()); // { done: false, value: 'bar'} 
console.log(iter.next()); // { done: false, value: 'baz' }
console.log(iter.next()); // { done: true,  value: undefined }

注意,迭代器维护着一个指向可迭代对象的引用,因此迭代器会阻止垃圾回收程序回收可迭代对象。

生成器

生成器是 ECMAScript6 新增的一个极为灵活的结构,拥有在一个函数块内暂停和恢复代码执行的能力。这种新能力具有深远的影响,比如,使用生成器可以自定义迭代器和实现协程。

生成器的形式是一个函数,函数名称前面加一个星号(*)表示它是一个生成器。只要是可以定义函数的地方,就可以定义生成器。

// 生成器函数声明
functiongeneratorFn() {} 

// 生成器函数表达式
let generatorFn = function* () {} 

// 作为对象字面量方法的生成器函数
let foo = { 
generatorFn() { } 
} 

// 作为类实例方法的生成器函数
class Foo { 
generatorFn() { }
}

// 作为类静态方法的生成器函数
class Bar { 
    staticgeneratorFn() { }
}

注意亮点:1、箭头函数不能用来定义生成器函数;2、标识生成器函数的星号*不受两侧空格的影响。

调用生成器函数会产生一个生成器对象。生成器对象一开始处于暂停执行(suspended)的状态。与迭代器相似,生成器对象也实现了 Iterator 接口,因此具有 next() 方法。调用这个方法会让生成器开始或恢复执行。

next() 方法的返回值类似于迭代器,有一个 done 属性和一个 value 属性。函数体为空的生成器函数中间不会停留,调用一次 next() 就会让生成器到达 done: true 状态。

yield 中断执行

yield 关键字可以让生成器停止和开始执行,也是生成器最有用的地方。生成器函数在遇到 yield 关键字之前会正常执行。遇到这个关键字后,执行会停止,函数作用域的状态会被保留。停止执行的生成器函数只能通过在生成器对象上调用 next() 方法来恢复执行。

functiongeneratorFn() {
    yield;
}
let generatorObject = generatorFn();
console.log(generatorObject.next()); // {done: false, value: undefined}
console.log(generatorObject.next()); // {done: true,  value: undefined}

此时的 yield 关键字有点像函数的中间返回语句,它生成的值会出现在 next() 方法返回的对象里。通过 yield 关键字退出的生成器函数会处在 done: false 状态;通过 return 关键字退出的生成器函数会处于 done: true 状态。

functiongeneratorFn() {
    yield 'foo';
    yield 'bar';
    return 'baz';
}
let generatorObject = generatorFn();
console.log(generatorObject.next());    // {done: false, value: 'foo'}
console.log(generatorObject.next());    // {done: false, value: 'bar'}
console.log(generatorObject.next());    // {done: true,  value: 'baz'}

生成器函数内部的执行流程会针对每个生成器对象区分作用域。在一个生成器对象上调用 next() 不会影响其他生成器。

yield 关键字只能在生成器函数内部使用,用在其他地方会抛出错误。类似函数的 return 关键字,yield 关键字必须直接位于生成器函数定义中,出现在嵌套的非生成器函数中会抛出语法错误。

functiongeneratorFn() {
    yield 'foo';
    yield 'bar';
    return 'baz';
}
let generatorObject1 = generatorFn();
let generatorObject2 = generatorFn();
console.log(generatorObject1.next());   // {done: false, value: 'foo'}
console.log(generatorObject2.next());   // {done: false, value: 'foo'}
console.log(generatorObject2.next());   // {done: false, value: 'bar'}
console.log(generatorObject1.next());   // {done: false, value: 'bar'}

补充

迭代器是一个可以由任意对象实现的接口,支持连续获取对象产出的每一个值。任何实现 Iterable 接口的对象都有一个 Symbol.iterator 属性,这个属性引用默认迭代器。默认迭代器就像一个迭代器工厂,也就是一个函数,调用之后会产生一个实现 Iterator 接口的对象。

迭代器必须通过连续调用 next() 方法才能连续取得值,这个方法返回一个 IteratorObject 。这个对象包含一个 done 属性和一个 value 属性。前者是一个布尔值,表示是否还有更多值可以访问;后者包含迭代器返回的当前值。这个接口可以通过手动反复调用 next() 方法来消费,也可以通过原生消费者,比如 for-of 循环来自动消费。

生成器是一种特殊的函数,调用之后会返回一个生成器对象。生成器对象实现了 Iterable 接口,因此可用在任何消费可迭代对象的地方。生成器的独特之处在于支持 yield 关键字,这个关键字能够暂停执行生成器函数。使用 yield 关键字还可以通过 next() 方法接收输入和产生输出。在加上星号之后,yield 关键字可以将跟在它后面的可迭代对象序列化为一连串值。

近期文章
title: JavaScript 变量提升
time: 2024-03-02
tag: #前端#JavaScript#浏览器
title: 美化 VSCode
time: 2024-01-31
tag: #IDE