自定义迭代器
上篇文章讲了迭代器的基本运用和协议规范,不熟悉的话,快去看看,超好懂的。 👉 JS一篇文章讲透迭代器-超好懂
说了那么多原理,就是为自定义做准备的
先整理下迭代器的协议
- 迭代器是迭代可迭代对象的关键,而它是从迭代对象中获取的。迭代对象有个
Symbol.iterator属性,这个属性指向一个函数,调用该函数,就可以得到对应的迭代器 - 拿到迭代器之后,可以通过调用迭代器
next的方法,来逐个遍历每个元素 - 迭代过程返回的数据结构为
{done,value},其中的done表示是否迭代结束,如果done === false,即没有迭代结束;如果done === true,即迭代结束。其中的value,是每次迭代返回的真正的值。
OK,我们来看具体代码实现
class Counter {
constructor(count) {
this.count = count;
}
[Symbol.iterator]() {
let index = 0;
return {
next: () => {
if (index < this.count) {
return {
done: false,
value: index++
}
} else {
return {
done: true,
value: null
}
}
}
}
}
}
这里声明了一个class,构造函数接收了个参数,表示待会迭代多少次。关键点在Symbol.iterator,这个属性指向一个函数,调用这个函数就能得到对应的迭代器。
迭代器中有个next属性,它也是一个函数,并且调用之后会返回一个{done,value}。注意其中的done的值,迭代没有结束的时候为false,迭代结束了为true。
应该不难理解我为啥要这么写吧,全是按照迭代器协议来写的
来看看这个自定迭代器是否生效:
const counter = new Counter(4);
for (const c of counter) {
console.log(c);
}
// 0
// 1
// 2
// 3
实例化的时候,传进去’ 4 ‘,之后的迭代也迭代4次,并且输出的值是每次迭代的序号。
完全符合预期😄
const setCounter = new Set(counter);
console.log(setCounter);
// Set(4) { 0, 1, 2, 3 }
用可迭代对象来创建一个set实例化对象,输出的结果显示,set中的每个元素都是迭代对象的每个元素
结果也是符合预期😄
再来做几个骚操作:
const arrayCounter = [...counter];
console.log(arrayCounter);
// [ 0, 1, 2, 3 ]
const [a, b] = counter;
console.log(a);
console.log(b);
// 0
// 1
数组解构和拓展操作符都是基于可迭代对象的
读者也可以用自定义迭代器尝试其他的原生语言结构,有助于理解记忆上面JS中涉及到迭代器的应用
迭代器加餐
其实,你会了上面的部分,就可以应付开发中的迭代器常见问题。下面说点迭代器的边角料
迭代器特性
我们来观察一段代码
const array = ['z', 'o', 'r', 'a'];
const iteratorArray1 = array[Symbol.iterator]();
const iteratorArray2 = array[Symbol.iterator]();
console.log(iteratorArray1.next());
console.log(iteratorArray1.next());
console.log(iteratorArray2.next());
// { value: 'z', done: false }
// { value: 'o', done: false }
// { value: 'z', done: false }
这里用了原生的迭代器做例子。显示从array中获取了两个迭代器,分别开始迭代。可以看到这两个迭代器是互不影响的。
这时iteratorArray2迭代器现在读取的是第一个元素,如果再调用next,读取到的值必然为’o‘。不过在这之前,对数组做点修改
array.splice(1, 0, 'zenos');
console.log(array);
// [ 'z', 'zenos', 'o', 'r', 'a' ]
在array的第二个位置,插入了一个’zenos‘的字符串。
可以猜下iteratorArray2接下来读到的元素是什么
console.log(iteratorArray2.next());
// { value: 'zenos', done: false }
是的,数组上的修改,立马就能在迭代器中反映出来。这就是迭代器的实时性。
总结一下迭代器的特性:独立性、实时性、一次性
读者:这个一次性是指什么?
作者:一次性就是指迭代器只能遍历迭代对象一次,不能回头遍历第二次
读者:哦,迭代器中没有回头的API,有的只是不断往下的next
作者:是的
迭代器的中断
在for-of遍历过程中,可以通过break;或者throw中断遍历。中断遍历会调用迭代器的return方法。这是迭代器的又一个API。
class Counter {
constructor(count) {
this.count = count;
}
[Symbol.iterator]() {
let index = 0;
return {
next: () => {
if (index < this.count) {
return {
done: false,
value: index++
}
} else {
return {
done: true,
value: null
}
}
},
return: () => {
console.log('exist early');
return {
done: true
}
}
}
}
}
上面自定义迭代器中,定义了一个return函数,当其被调用的时候,就会输出"exist early"
const counter = new Counter(4);
for (const c of counter) {
console.log(c);
if (c === 2) {
break;
}
}
// 0
// 1
// 2
// exist early
结果符合预期😁
再来一个骚操作:
const [a, b] = counter;
console.log(a);
console.log(b);
// exist early
// 0
// 1
数组的解构也是一个迭代的过程,而这里只迭代了前两个就中断了迭代
但是迭代器中断之后,迭代器是否被关闭了呢?
const array = [1, 2, 3, 4];
const iteratorArray = array[Symbol.iterator]();
for (const item of iteratorArray) {
console.log(item);
if (item === 2) {
break;
}
}
console.log(iteratorArray.next());
// 1
// 2
// { value: 3, done: false }
要看懂上面的代码,还需要知道一个概念:原生的迭代器本身也有Symbol.iterator属性,调用这个方法也会返回迭代器,这个迭代器就是自己。
const array = [1, 2, 3, 4];
const iteratorArray = array[Symbol.iterator]();
const iteratorArray2 = iteratorArray[Symbol.iterator]();
//迭代器的Symbol.iterator函数,返回了自己
console.log(iteratorArray === iteratorArray2);
这也就是为什么
for-of中把迭代对象的迭代器作为迭代的数据源也是可以的
然后迭代iteratorArray的过程中,调用了break中断了迭代。在后面又调用了迭代器的next,可以看到输出结果恰好是中断点的下一个元素。
也就是说中断迭代器,并不会一定会导致迭代器关闭,这是不一定的。
for-of迭代数据的时候,会调用对象的Symbol.iterator方法。
而正是因为迭代器的
Symbol.iterator函数返回了自己,所以就能通过再次调用iteratorArray.next来证明 “ 中断不会导致迭代器关闭 ” 这个结论。
读者:你为了说清楚这个结论,不惜引入了一个新概念“迭代器的Symbol.iterator函数返回了自己”
作者:是啊,这是没有办法的事情啊。你看懂了吗
读者:嗯,还得多想想
除了break可以中断迭代过程,throw也可以办得到,这个就留给读者自己去尝试吧
总结
- 自定义迭代器
- 迭代器特性:独立性,实时性,一次性
- 迭代器的
Symbol.iterator函数会返回自己 - 迭代过程被中断后,并不一定会导致迭代器的关闭
- 有不明白的地方,留言告诉我