持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第3天,点击查看活动详情
前言
在 ES6 中,数组原型提供了很多迭代方法,some、every、filter、forEach、map,还有归并方法 reduce,本质上也是迭代、这些迭代方法往往能满足我们的日常开发,但是在有些时候,并不能总是这么有效
除了数组之外的其他任意数据类型是否都能迭代呢?
能否自定义迭代方式呢?
ES6 中出现了 迭代器 / 生成器 能够很好的解决这些问题
1. 迭代器模式
我们首先从迭代器模式入手,详细地介绍迭代器
迭代器的种类分为内部迭代器和外部迭代器
1.1 内部迭代器
在内部已经定义好了迭代规则,它完全控制整个迭代过程,外部只需要一次初始调用
type hasLengthProperty = {
[index: number]: any;
length: number;
};
function each<T extends hasLengthProperty>(
iterator: T, // 迭代的对象可以是任何具有 length 属性的内容
fn: (item: keyof T, index: number) => void
) {
for (let i = 0; i < iterator.length; i++) {
fn(iterator[i], i);
}
}
使用
each(
{
'0': 0,
'1': 1,
'2': 2,
length: 3,
},
function (item, index) {
console.log('item:' + item, 'index:' + index);
}
);
// item:0 index:0
// item:1 index:1
// item:2 index:2
这种迭代方式在调用的时候非常简单,外界不用关心迭代器内部的实现,但是如果同时迭代两个数组,这种方式就捉襟见肘了。比如判断两个数组是否全等,只能在 each 提供的接口 回调函数 中判断每一项是否全等。
因此,第二种方法,外部迭代器就应运而生。
1.2 外部迭代器
可以显示地执行迭代下一个元素
type hasLengthProperty = {
[index: number]: any;
length: number;
};
class Iterator<T extends hasLengthProperty> {
iterator: T;
currIdx: number = 0;
constructor(iterator: T) {
this.iterator = iterator;
}
next() {
const item = this.iterator[this.currIdx++];
return {
value: item,
done: item ? false : true, // 迭代完所有元素之后就可以标识迭代进程的结束
};
}
}
使用:
const iterator = new Iterator([1, 2, 3]);
iterator.next() // {value: 1, done: false}
iterator.next() // {value: 2, done: false}
iterator.next() // {value: 3, done: false}
iterator.next() // {value: undefined, done: true}
通过这个例子可以很好的看出,外部迭代器的优势,将迭代的权利反转交给外部,由外部控制整个迭代的过程
外部迭代器对比内部迭代器还有一个很大的优势,当需要终止整个迭代过程,外部迭代器可以很简单地实现,而内部迭代器需要在迭代的地方手动 break 或者 return,在一些 es6 内置的数组方法中,不能通过这两个关键字终止迭代,只能 throw Error 和 try ~ catch
try {
[1, 2, 3, 4].forEach(item => {
if (item === 3) {
throw new Error('终止迭代');
}
console.log(item);
});
} catch (e: any) {
console.log(e.message);
}
// 1
// 2
// 终止迭代
2. ES6 的迭代器
2.1 迭代器
定义:实现了 next 方法,在 next 方法里实现了迭代的过程
就像上面的例子,我们可以对任意一种数据结构自定义迭代规则,封装在 next 方法里,拥有 next 方法控制迭代过程的对象我们就可以说它是迭代器了
2.2 可迭代对象
定义:实现了可迭代协议的对象。包含的元素都是有限的,而且都具有无歧义的遍历顺序
与迭代器的区别:迭代器可以看作是可迭代对象构造出来的结果
可迭代协议:
- 实现了
[Symbol.iterator]为key的方法,且这个方法返回了一个迭代器对象,也就是实现了 next 方法的对象
一些常见的可迭代对象
- 数组
- Set
- Map
- 类数组:字符串、arguments、NodeList
而这些可迭代对象具有相同的特性
- for-of 循环
- 数组解构
- 扩展操作符
- Array.from()
- 创建集合(能被 Set new)
- 创建映射(能被 Map new)
- Promise.all()接收由期约组成的可迭代对象
- Promise.race()接收由期约组成的可迭代对象
- yield*操作符,在生成器中使用
字符串也是可迭代对象
[...'123456'];
// ['1', '2', '3', '4', '5', '6']
在可迭代对象调用迭代器工厂函数
[][Symbol.iterator] // 迭代器工厂函数
// ƒ values() { [native code] }
[][Symbol.iterator]() // 返回迭代器
执行迭代器工厂函数返回结果是迭代器: 具有 next 方法
可迭代对象和迭代器通过 Symbol.iterator 联系在一起,当我们调用 next 方法返回的对象 {value: any; done: boolean} value 包含可迭代对象的下一个值(done 为 false),或者 undefined(done 为 true),表示迭代过程的结束
2.3 自定义迭代器
安装上文的讲述,我们知道了什么迭代器,那我们可以根据这个规范自定义迭代器
const obj = {
value: '123456',
[Symbol.iterator]: function () {
let index = 0;
return {
next: () => {
const value: string | undefined = this.value[index++];
return { value, done: value ? false : true };
},
};
},
};
for (const i of obj) {
console.log(i);
}
// 1
// 2
// 3
// 4
// 5
// 6
这些可迭代对象具有共同的特性,比如 for~of 循环、... 扩展运算本质都是调用了可迭代对象下面的 Symbol.iterator 方法,隐式执行了 next 方法的过程
封装 Iterator
type hasLengthProperty = {
[index: number]: any;
length: number;
};
class Iterator<T extends hasLengthProperty> {
private iterator: T;
constructor(iterator: T) {
this.iterator = iterator;
}
[Symbol.iterator]() {
let index = 0;
return {
next: () => {
let value = this.iterator[index++];
return {
value,
done: value ? false : true,
};
},
[Symbol.toStringTag]: 'Iterator',
};
}
}
for (const i of new Iterator([1, 2, 3, 4])) {
console.log(i);
}
// 1
// 2
// 3
// 4
for~of 循环会忽略掉 done: true 的内容,在这个例子中封装方法中并不完美,如果传入内容的某一项元素为 undefined | null | '' | false | NaN 则会终止迭代,请读者自行修改,可以从 index 和 length 这个角度做文章
注意:Symbol.iterator 属性引用的工厂函数 会返回相同的迭代器
const arr: number[] = [1, 2, 3, 4];
const iterator = arr[Symbol.iterator]();
iterator[Symbol.iterator]() === iterator; // true
for (const i of iterator) {
console.log(i);
}
// 1
// 2
// 3
// 4
这个例子还说明迭代器中依然内置了可迭代协议 Symbol.iterator
改写 Iterator
class Iterator<T> {
...
[Symbol.iterator](): any {
...
return {
...,
[Symbol.iterator]: Iterator.prototype[Symbol.iterator].bind(this),
}
}
}
我们在这里给每个迭代器对象都添加了可迭代协议,也就是说,自定义迭代器也是可迭代对象
const iterator = new Iterator([1, 2, 3])
[Symbol.iterator]()
[Symbol.iterator]()
[Symbol.iterator]()
[Symbol.iterator]()
[Symbol.iterator]()
[Symbol.iterator]();
for (const i of iterator) {
console.log(i);
}
// 1
// 2
// 3
2.4 终止迭代器
迭代器对象中可选的 return方法用于指定在迭代器提前关闭时执行的逻辑。
我们在自定义迭代器中添加 return 方法
class Iterator<T> {
...
[Symbol.iterator](): any {
...
return {
...,
return() {
console.log('终止迭代');
return { done: true, value: undefined };
},
}
}
}
可以关闭迭代器的情况可能为:
- for-of 循环通过
break、continue、return或throw提前退出; - 未使用完的
解构对象中的元素
const iterator1 = new Iterator([1, 2, 3])[Symbol.iterator]();
const iterator2 = new Iterator([1, 2, 3])[Symbol.iterator]();
const iterator3 = new Iterator([1, 2, 3])[Symbol.iterator]();
// 1.
const [a, b] = iterator1;
// 输出'终止迭代',会触发 return 方法
// 2.
for (const i of iterator2) {
if (i > 1) {
break;
}
console.log(i);
}
// 1
// 终止迭代
// 3.
try {
for (const i of iterator3) {
if (i > 1) {
throw new Error();
}
console.log(i);
}
} catch (e) {}
// 1
// 终止迭代
如上面的代码所示,还有一些内置的可迭代对象也会触发对应的 return 方法
const arr = [1, 2, 3];
const iter = arr[Symbol.iterator]();
iter.return = function () {
console.log('终止迭代');
return { done: true, value: undefined };
};
for (const i of iter) {
if (i > 2) {
break;
}
console.log(i);
}
// 1
// 2
// 终止迭代
如果迭代器没有关闭,则还可以继续从上次离开的地方继续迭代。比如,数组的迭代器就是不能关闭的,但是可以终止迭代:
const iterator = [1, 2, 3][Symbol.iterator]();
iter.return = function () {
console.log('终止迭代');
return { done: true, value: undefined };
};
for (const i of iterator) {
if (i > 1) {
break;
}
console.log(i);
}
// 1
// 终止迭代
for (const i of iterator) {
console.log(i);
}
// 3
因为 return方法是可选的,所以并非所有迭代器都是可关闭的。不过,仅仅给一个不可关闭的迭代器增加这 个方法并不能让它变成可关闭的。这是因为调用 return 不会强制迭代器进入关闭状态。即便如此, return 方法还是会被调用。
而我们自定义的 Iterator 在这里表现与这些内置方法有很大的差异
const iterator = new Iterator([1, 2, 3])[Symbol.iterator]();
iterator.return = function () {
console.log('终止迭代');
return { done: true, value: false };
};
for (const i of iterator) {
if (i > 1) {
break;
}
console.log(i);
}
// 1
// 终止迭代
for (const i of iterator) {
console.log(i);
}
// 1
// 2
// 3
相同的迭代器执行过程独立,因为我们自制的迭代器会在每一次执行 for~of 的时候会重新调用 [Symbol.iterator],index 为两个不同的变量
3. 总结
一个任何类型的数据结构只要添加了 [Symbol.iterator],并且满足可迭代协议,那我们就可以改造不可迭代的数据结构变为可迭代的对象