迭代器(Iterator)
迭代器是借鉴C++等语言的概念,迭代器的原理就像指针一样,它指向数据集合中的某个元素,你可以获取它指向的元素,也可以移动它以获取其它元素。
JS中的迭代器则是专门为了遍历这一操作设计的。每次获取到的迭代器总是初始指向第一个元素,并且迭代器只有next()一种行为,直到获取到数据集的最后一个元素。我们无法灵活移动迭代器的位置,所以,迭代器的任务,是按顺序遍历(读取)数据集中的元素。
迭代器是一个对象,JS规定,迭代器必须实现next()接口,它应该返回当前元素并将迭代器指向下一个元素,返回的对象格式为{ value: 元素值, done: 是否遍历结束 },其中,done是一个布尔值。
我们试着来实现一个迭代器:
const iterator = { // 迭代器是一个对象,先创建一个对象
i: 0,
next() { // 实现next()接口
if (this.i > 10) return {value: undefined, done: true };
return { value: this.i++, done: false };// this.i++ 将迭代器指向下一个元素
}
}
//手动使用迭代器
console.log(iter.next()); //{ value: 0 }
console.log(iter.next()); //{ value: 1 }
// 自动调用迭代器
while (true) {
let item = iterator.next();
if (!item.done) {
console.log(item.value); //打印从2到10
} else {
break;
}
}
这是一个符合规定的迭代器,但是把它单独拿出来并没有什么意义,迭代器的意义是附着在对象上,让一个对象,或者数据结构成为可迭代对象。
迭代器接口与可迭代对象
迭代器接口是我们获取对象的迭代器时默认调用的接口,一个实现了迭代接口的对象即是可迭代iterable对象。
对象的迭代器接口(方法)被挂在@@iterator属性上,例如Array.prototype[@@iterator](),
当需要对一个对象进行迭代时(比如开始用于一个for..of循环中),它的@@iterator方法都会在不传参情况下被调用,返回迭代器用于获取要迭代的值。
一些内置类型拥有默认的迭代器行为,其他类型(如 Object)则没有。下面的内置类型拥有默认的@@iterator方法,即它们是内置的可迭代对象:
Array.prototype[@@iterator]()TypedArray.prototype[@@iterator]()String.prototype[@@iterator]()Map.prototype[@@iterator]()Set.prototype[@@iterator]()DOM中的NodeList对象。函数的arguments属性。
Symbol.iterator则指向了对象的迭代器接口([@@iterator]()),调用时也会返回迭代器,[Symbol.iterator](),可以用它获取或设置对象的迭代器。
我们来尝试获取一下数组的迭代器:
arr = [1, '2', 3];
let arrIt = arr[Symbol.iterator](); // 获取数组迭代器,Symbol.iterator要使用[Symbol.iterator]包裹起来
console.log(arrIt.next()); //{ value: 1, done: false }
console.log(arrIt.next()); //{ value: '2', done: false }
console.log(arrIt.next()); //{ value: 3, done: false }
console.log(arrIt.next()); //{ value: undefined, done: true }
我们看到成功调用了next()方法,说明已经获取了数组的迭代器。
实际上数组的
@@iterator属性和Array.prototype.values()属性的初始值是同一个函数对象。 即arr[Symbol.iterator]会返回values()函数,而values()函数则返回一个迭代器。
是不是恍然大明白!
自己实现一个数组的迭代器:
const arr = [10, 20, 30];
// 数组迭代器对象
const arrIterator = {
i: 0,
next() {
if (this.i < arr.length) {
return { done: false, value: arr[this.i++] };
}
return { done:true, value: undefined };
}
}
console.log(arrIterator.next()); // {done: false, value: 10}
console.log(arrIterator.next()); // {done: false, value: 20}
console.log(arrIterator.next()); // {done: false, value: 30}
console.log(arrIterator.next()); // {done: true, value: undefined}
甚至你可以更改数组的原生遍历器
const arr = [10, 20, 30];
// 数组迭代器对象
const arrIterator = {
i: 0,
next() {
if (this.i < arr.length) {
return { done: false, value: arr[this.i++]+1 };
}
return { done:true, value: undefined };
}
}
arr[Symbol.iterator] = () => arrIterator; // 创建一个迭代器接口(即方法)返回迭代器对象,注意迭代器接口和迭代器对象的区别
// 或
Array.prototype[Symbol.iterator] = () => arrIterator; // 这种方式会改掉所有数组的迭代器
for (const item of arr) {
console.log(item); // 11 21 31
}
但是,别瞎改,这没有意义。
迭代器的作用
- 为各种数据结构,提供一个统一的、简便的访问接口;
实现这个接口的数据结构都可被遍历,即遍历(访问)数据结构的统一的一种方法。
- 使得数据结构的成员能够按某种次序排列;
将你定义的数据结构可以以有序的方式被读取,例如你定义的[1, 2, 3],展示到页面时也是按顺序的。
- ES6 创造了一种新的遍历命令for…of循环,Iterator 接口主要供for…of消费。
自定义可迭代对象
根据迭代器的作用,意味着我们可以把非可迭代对象变为可迭代对象,例如:
我们开头举例的迭代器就是一个对象,有自己的属性,它不可迭代,我们来改一下
const iterator = { // 迭代器是一个对象,先创建一个对象
i: 0,
next() { // 实现next()接口
if (this.i > 10) return {value: undefined, done: true };
return { value: this.i++, done: false };// this.i++ 将迭代器指向下一个元素
}
// 实现对象的迭代器接口,然后它就可以被遍历了
[Symbol.iterator]() {
return this; // 因为这个对象本身就是迭代器对象,所以可以用自身作为自己的迭代器,你也可以自己写迭代器
}
}
for (const item of iterator) {
console.log(item); // 0,1,2,...,10
}
给对象实现迭代器接口后就可以遍历对象了,类似地,你还可以给类或方法添加迭代器接口,例如:
class iterableList {
constructor(data) {
this.data = data;
}
[Symbol.iterator]() {
...
}
}
const list = new iterableList([]);
for(let item of list) {
console.log(item);
}
可迭代对象的意义
可迭代对象作为数组的扩充,在以前,要操作一组数据,只有数组和对象这种形式,对象的能力很有限,所以大部分场景我们需要使用数组,非数组对象必须通过某种方式转化为数组,完成之后,还可能需要还原成原来的结构,这种繁琐的来回切换很不理想。有了可迭代对象的概念,可以对更多的数据结构直接进行访问遍历,而无需转化为数组,某些操作可以接受一个可迭代对象作为参数,传入数组、Set、Map以及其他任何实现了可迭代接口的对象也能正常处理。例如:
// 求和
function sumInIterable(iterable){
let sum = 0;
for(let num of iterable){
sum+=num;
}
return sum;
}
上面的方法传入任意可迭代对象都能进行正确的处理。
数组到可迭代对象的提升,代表了方法的通用性的提升。请问你在设计方法的时候,会考虑能否使用可迭代对象代替数组吗?你知道Promise.all()他传入的参数就是可迭代对象吗,我们大多数人是不是只知道它可以传入数组呢。
Promise.all(iterable)
一句话总结:
可迭代对象作为数组的扩充,可以对更多的数据结构进行直接处理,当作为方法的参数时,可以提高方法的通用性。
消费可迭代对象的场合(调用Iterator接口)
for...of语法...iterable:扩展运算符和解构赋值Generator yield*语法Map,Set,WeakMap,WeakSet的构造器,比如new Map([['a',1],['b',2]]))Array.from(iterable)和Object.fromEntries(iterable)Promise.all(iterable)和promist.race(iterable)
所以,从现在起不要再认为它们接受的仅仅是数组参数啦,数组参数只是可迭代对象中的一种。
总结
迭代器(iterator)是ES6提出的给不同数据结构提供统一的访问接口的一种机制,有了它之后,可以很容易地创造出更多的数据结构,如set、Map,可以写出更通用的接口,如Promise.all(iterable)、Object.fromEntries(iterable)等。
细心的小伙伴可能发现了,好多ES6+的新特性都与迭代器有关,现在应该能理解“为各种数据结构提供统一的访问接口的一种机制”这句话了吧。