迭代器
1、什么是迭代?
迭代的英文为 iteration, 意思为“重复” 或 “再来”。 在软件开发领域中, “迭代” 的意思是按照顺序反复多次执行一段程序, 通常会有明确的终止条件。
计数循环就是一种最简单的迭代
for (let i = 0; i <= 10; i++) {
console.log(i)
}
循环是迭代机制的基础,这是因为它可以指定迭代的次数,以及每次迭代要执行什么操作。每次循环都会在下一次迭代开始之前完成,而每次迭代的顺序都是事先定义好的。
迭代会在一个有序的集合上进行。数组是javascript中有序集合的最典型例子。
let collection = ['foo', 'bar', 'baz'];
for (let index = 0; index < collection.length; ++index) {
console.log(collection[index]);
}
因为数组有已知的长度,且数组每一项都可以通过索引获取,所以整个数组可以通过递增索引来遍历。
由于如下原因,通过这种循环来执行例程并不理想。
-
迭代之前需要事先知道如何使用数据结构。数组中的每一项都只能先通过引用取得数组对象,然后再通过 [] 操作符取得特定索引位置上的项。这种情况并不适用于所有数据结构。
-
遍历顺序并不是数据结构固有的。通过递增索引来访问数据是特定于数组类型的方式,并不适用于其他具有隐式顺序的数据结构
Array.prototype.forEach()
ES5新增的Array.prototype.forEach() 方法, 向通用的迭代需求迈进了一大步,虽然还不是十分理想。
let arr = [1,2,3];
arr.forEach((v) => {console.log(v)})
// 1
// 2
// 3
2、迭代器模式
迭代器模式描述了一个方案, 将有些结构称为“可迭代对象”(iterable),因为它们实现了正式的 Iterable 接口,而且可以通过迭代器 Iterator 消费。
可迭代对象是一种抽象的说法。基本上,可以把可迭代对象理解成数组或集合这样的集合类型的对象。它们包含的元素都是有限的,而且都具有无歧义的遍历顺序:
// 数组的元素是有限的
// 递增索引可以按序访问每个元素
let arr = [3, 1, 4];
// 集合的元素是有限的
// 可以按插入顺序访问每个元素
let set = new Set().add(3).add(1).add(4);
不过,可迭代对象不一定是集合对象,也可以是仅仅具有类似数组行为的其他数据结构,比如计数循环。该循环中生成的值是暂时性的,但循环本身是在执行迭代。计数循环和数组都具有可迭代对象的行为。
任何实现 Iterable 接口的数据结构都可以被实现 Iterator 接口的结构“消费”(consume)。迭代器(iterator)是按需创建的一次性对象。每个迭代器都会关联一个可迭代对象,而迭代器会暴露迭代其关联可迭代对象的 API。迭代器无须了解与其关联的可迭代对象的结构,只需要知道如何取得连续的值。这种概念上的分离正是 Iterable 和 Iterator 的强大之处。
好的 大部分人看到这里已经晕了 稍微整理以下几个概念
可迭代对象( iterable ) 类似数组和集合这样子的对象
迭代器( iterator ) 一个按需创建的一次性对象,每个迭代器关联一个可迭代对象,暴露相关联的可迭代对象的API
2.1 可迭代协议
要实现可迭代协议(Iterable接口) 需满足两个条件
- 支持迭代的自我识别能力
- 创建实现 Iterable 接口的对象的能力
在 ECMAScript 中,这意味着必须暴露一个属性作为“默认迭代器”,而且这个属性必须使用特殊的 Symbol.iterator 作为键。这个默认迭代器属性必须引用一个迭代器工厂函数,调用这个工厂函数必须返回一个新迭代器。
以下类型都实现了 Iterable 接口
- 字符串
- 数组
- 映射
- arguments 随想
- NodeList 等 DOM 集合类型
let str = "123";
console.log(str[Symbol.iterator]);//ƒ [Symbol.iterator]() { [native code] }
console.log(str[Symbol.iterator]());//StringIterator {}
let arr = [];
console.log(arr[Symbol.iterator]);//ƒ values() { [native code] }
console.log(arr[Symbol.iterator]());//Array Iterator {}
let map = new Map().set("a", 1).set("b", 2).set("c", 3);
console.log(map[Symbol.iterator]);//ƒ entries() { [native code] }
console.log(map[Symbol.iterator]());//MapIterator {'a' => 1, 'b' => 2, 'c' => 3}
let set = new Set().add("a").add("b").add("c");
console.log(set[Symbol.iterator]);//ƒ values() { [native code] }
console.log(set[Symbol.iterator]());//SetIterator {'a', 'b', 'c'}
let els = document.querySelectorAll("div");
console.log(els[Symbol.iterator]);//ƒ values() { [native code] }
console.log(els[Symbol.iterator]());//Array Iterator {}
以上类型都实现了迭代器工厂函数, 并且调用这个函数会生成一个迭代器
如果对象原型链上的父类实现了 Iterable 接口,那这个对象也就实现了这个接口
2.2 迭代器协议
迭代器是一种一次性使用的对象, 用于迭代与其关联的可迭代对象。 迭代器API使用 next() 方法在可迭代对象中遍历数据。每次成功则调用 next() , 都会返回一个 IteratorResult 对象, 其中包含迭代器返回的下一个值, 如果不调用 next() 的话则无法知道迭代器的当前位置。
next() 方法返回的 IteratorResult 包含两个属性 done 和 value。 done 是一个布尔值表示是否可以再次调用 next()去获取下一个值; value 包含了可迭代对象的下一个值(done为false的时候), 活着是 undefiend ( done为 true 的时候 ) 。
done为 true 状态的时候, 意为 “耗尽”
// 可迭代对象
let arr = ['foo', 'bar']
// 迭代器工厂函数
console.log(arr[Symbol.iterator]); // f value() { [native code] }
// 迭代器
let iter = arr[Symbol.iterator]();
console.log(iter); // Array Itertor {}
// 迭代
iter.next();// {done: false, value: 'foo'}
iter.next();// {done: fasle, value:' bar'}
iter.next();// {done: true, value: undefined }
这里通过创建迭代器并调用 next() 方法按顺序迭代了数组,直至不再产生新值。迭代器并不知道
怎么从可迭代对象中取得下一个值,也不知道可迭代对象有多大。只要迭代器到达 done: true 状态,后续调用 next() 就一直返回同样的值了:
let arr = ['foo'];
let iter = arr[Symbol.iterator]();
console.log(iter.next()); // { done: false, value: 'foo' }
console.log(iter.next()); // { done: true, value: undefined }
console.log(iter.next()); // { done: true, value: undefined }
console.log(iter.next()); // { done: true, value: undefined }
每个迭代器都表示对可迭代对象的一次性有序遍历。不同迭代器的实例相互之间没有联系,只会独立地遍历可迭代对象:
let arr = ['foo', 'bar'];
let iter1 = arr[Symbol.iterator]();
let iter2 = arr[Symbol.iterator]();
console.log(iter1.next()); // { done: false, value: 'foo' }
console.log(iter2.next()); // { done: false, value: 'foo' }
console.log(iter2.next()); // { done: false, value: 'bar' }
console.log(iter1.next()); // { done: false, value: 'bar' }
&emps; 迭代器并不与可迭代对象某个时刻的快照绑定,而仅仅是使用游标来记录遍历可迭代对象的历程。如果可迭代对象在迭代期间被修改了,那么迭代器也会反映相应的变化:
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 }
2.3 自定义迭代器
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
for (let i of counter) { console.log(i); }
// 1
// 2
// 3
2.4 提前终止迭代器
可选的 return() 方法用于指定在迭代器提前关闭时执行的逻辑。执行迭代的结构在想让迭代器知道它不想遍历到可迭代对象耗尽时,就可以“关闭”迭代器。可能的情况包括:
- for-of 循环通过 break 、 continue 、 return 或 throw 提前退出;
- 解构操作并未消费所有值。
return() 方法必须返回一个有效的 IteratorResult 对象。简单情况下,可以只返回 { done: true } 。这个返回值只会用在生成器的上下文中。
&emsp如下面的代码所示,内置语言结构在发现还有更多值可以迭代,但不会消费这些值时,会自动调用return() 方法。
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 };
}
};
}
}
let conuter1 = new Counter(5);
for(let i of counter) {
if(i > 2) {
break;
}
console.log(i)
}
// 1
// 2
// Exiting early
因为 return() 方法是可选的,所以并非所有迭代器都是可关闭的。**要知道某个迭代器是否可关闭,可以测试这个迭代器实例的 return 属性是不是函数对象。**不过,仅仅给一个不可关闭的迭代器增加这个方法并不能让它变成可关闭的。这是因为调用 return() 不会强制迭代器进入关闭状态。即便如此,return() 方法还是会被调用。