JavaScript中的迭代器是一种常见的机制,它在数据遍历和一些常用的JavaScript操作中扮演着重要的角色。你可能已经熟悉一些使用迭代器的常见情况,比如数组的扩展运算符(spread operator
)和数组的解构赋值,这些都是通过迭代器来实现的,那么接下来让我们来具体的了解一下JavaScript
中的迭代器吧!
迭代器解决了什么问题?
也许你已经知道了遍历数组可以使用forEach
循环,遍历对象可以使用for in
循环。但是,从ES6开始添加了新的数据结构Map
、Set
,那么对于这些不同的数据结构,就需要一个统一的方式 (接口)去遍历这些数据,那么对于这些新数据的迭代方式,相应的解决方案就是Iterator
(迭代器),具体的表现形式如下:
- 迭代器为JavaScript提供了一个统一的遍历数据的方式,不论是数组、对象、Map以及Set
- 带来了基于迭代器实现的
for of
循环命令 - 使得数据结构的成员能够按某种次序排列,比如遍历对象的属性的时候按照特定的顺序遍历
在了解了迭代器解决了什么问题后,现在来具体了解一下迭代器到底是什么吧!
迭代器是什么?
首先来看下MDN官网对于迭代器的定义:
在 JavaScript 中,迭代器是一个对象,它定义一个序列,并在终止时可能返回一个返回值。更具体地说,迭代器是通过使用 next() 方法实现 Iterator protocol 的任何一个对象,该方法返回具有两个属性的对象: value,这是序列中的 next 值;和 done ,如果已经迭代到序列中的最后一个值,则它为 true 。如果 value 和 done 一起存在,则它是迭代器的返回值。------【MDN-迭代器】
看完上面这段话后是不是有点懵? 简单总结一下得到下面这些:
- 在JavaScript中迭代器是一个特殊的对象,这个对象具有一些独特的特征
- 拥有一个
next()
方法,每次调用都会返回一个对象 - 调用
next()
方法返回的对象具有两个值value
以及done
value
返回迭代序列 中下一次迭代返回的值,如果迭代结束返回undefined
。done
是一个布尔值,如果已经迭代到序列中的最后一个值,则为true
,否则返回false
。
- 拥有一个
- 在创建迭代器对象之后,可以通过调用
next()
方法对数据进行显示迭代。
可迭代协议
可迭代协议和迭代器是紧密相连的一个概念,首先来看下可迭代协议的概念:
可迭代协议允许 JavaScript 对象定义或定制它们的迭代行为,例如,在一个 for..of 结构中,哪些值可以被遍历到。一些内置类型同时是内置的可迭代对象,并且有默认的迭代行为,比如 Array 或者 Map,而其他内置类型则不是(比如 Object)。 为了可迭代,对象必须实现 @@iterator 方法,这意味着该对象(或者它原型链上的某个对象)必须具有带有 @@iterator 键的属性,该属性可通过常量 Symbol.iterator 获得。
- [Symbol.iterator]:一个无参数的函数,其返回值为一个符合迭代器协议的对象。
------【MDN-可迭代协议】
通过上文可以知道以下几点:
可迭代协议是什么?
- 可迭代协议(Iterable)定义了一个对象如何能够被迭代,用于告诉JavaScript如何在循环中遍历该对象的元素。
可迭代协议的特征
- 在JavaScript中,要实现可迭代协议,需要提供一个特殊的属性 作为"默认迭代器",而这个属性就是
Symbol.iterator
Symbol.iterator
属性是一个无参数的函数,其返回值为一个符合迭代器协议的对象(这个对象就是#2.迭代器是什么?
中所描述的对象)。- 通过调用
Symbol.iterator
属性(方法),可以获取到这个默认迭代器对象,通过这个对象就可以使用进行迭代的操作。 - 一个符合迭代器协议的对象需要实现迭代器接口,即具有
next()
方法。
原生可迭代与非原生可迭代
在了解完成可迭代协议后,我们就可以来看看原生可迭代与非原生可迭代的概念了。
原生可迭代指的是天生具有Symbol.iterator
方法的数据,比如:
- 字符串
- 数组
- Set
- Map
- arguments对象
- NodeList等DOM集合
而天生没有Symbol.iterator
方法的数据,就是非原生可迭代的,比如:
- 对象
- 数字
迭代器的基本使用
- 对数据调用
[Symbol.iterator]()
方法,得到可迭代的对象(#可迭代协议的特征-2
) - 对这个对象调用
next()
方法,直到done
的值为true
javascript
// 对数据调用[Symbol.iterator]()方法,得到可迭代的对象
const it = [1, 2, 3][Symbol.iterator]();
// 对这个对象调用next()方法,直到done的值为true
console.log(it.next()); // {value: 1, done: false}
console.log(it.next()); // {value: 2, done: false}
console.log(it.next()); // {value: 3, done: false}
console.log(it.next()); // {value: undefined, done: true}
实际写代码过程中,一般不会使用这种写法,通常情况下会使用for of循环来代替这种写法。
for of循环
- 一种基于迭代器实现的遍历数据的方式,内部依靠迭代器实现数据的迭代
for of
循环只会遍历出那些done为false时,对应的value值
javascript
let arr = [1,2,3];
for (const item of arr) {
console.log(item);
}
// 相当于
// 1. 获取可迭代对象
let it1 = arr[Symbol.iterator]();
// 2. 手动调用next()方法
let next = it1.next();
while(!next.done) {
console.log(next.value);
next = it1.next();
console.log(next);
}
- 同样
for...of
循环也支持break
和continue
关键字
javascript
// 1. 在 for...of 中使用 break 关键字 跳出整个for of循环
const arr = [1,2,3,4];
for (const item of arr) {
if (item === 2) {
break;
}
console.log(item);
}
// 2. 在 for...of 中使用 continue 关键字 跳出本次循环
const arr = [1,2,3,4];
for (const item of arr) {
if (item === 2) {
continue;
}
console.log(item);
}
自定义遍历非原生可迭代的数据
比如现在想要遍历一个对象,那么就可以手动实现一下[Symbol.iterator]
方法,通过这个方法就可以遍历对象了
- 遍历普通的对象
javascript
// 1. 准备一个测试对象
const obj = {
a: 1,
b: 2,
c: 3
}
// 2. 定义迭代器方法 [Symbol.iterator](),让 obj 变为一个可迭代的对象
obj[Symbol.iterator] = () => {
// 3. 定义一个计数标记 index
let index = 0
// 4. 返回一个对象,这个对象提供了next方法,切每次调用都会返回一个对象
return {
next() {
index++;
if(index === 1) {
return {
value: obj.a,
done: false
}
} else if (index === 2) {
return {
value: obj.b,
done: false
}
} else if (index === 3) {
return {
value: obj.c,
done: false
}
} else {
return {
value: undefined,
done: true
}
}
}
}
}
// 5. 测试
// 5.1 获取迭代器对象
const it = obj[Symbol.iterator]()
// 5.2 手动调用一次
let next = it.next()
while(!next.done) {
console.log(next.value)
next = it.next()
console.log(next);
}
// 当然也可以直接用 for...of 循环
for(let item of obj) {
console.log(item)
}
- 遍历有length和索引属性的对象
javascript
// 1. 准备一个测试对象
const obj = {
0: "value1",
1: "value2",
2: "value3",
length: 3,
};
// 2. 定义迭代器方法 [Symbol.iterator](),让 obj 变为一个可迭代的对象
obj[Symbol.iterator] = () => {
// 3. 定义一个计数标记 index
let index = 0;
// 4. 返回一个对象,这个对象提供了next方法,切每次调用都会返回一个对象
let value, done;
return {
next() {
if(index < obj.length) {
value = obj[index];
done = false;
} else {
value = undefined;
done = true;
}
index++;
return {
value,
done
}
}
}
}
// 5. 测试
// 5.1 获取迭代器对象
const it = obj[Symbol.iterator]()
// 5.2 手动调用一次
let next = it.next()
while(!next.done) {
console.log(next.value)
next = it.next()
console.log(next);
}
// 当然也可以直接用 for...of 循环
for(let item of obj) {
console.log(item)
}
- 对于这种有
length
和索引属性的对象,如果需要迭代里面的数据,实际上可以直接借用数组的迭代器方法来帮助迭代
javascript
// 1. 准备一个测试对象
const obj = {
0: "value1",
1: "value2",
2: "value3",
length: 3,
};
// 2. 测试 这里直接接用数组的迭代器方法给这个测试对象
obj[Symbol.iterator] = Array.prototype[Symbol.iterator];
for (const item of obj) {
console.log('item',item);
}
使用了Iterator的场景
打印FormData中的数据
使用FormData这个类实例化的对象无法正常打印
使用迭代器的方式才能查看里面的键值对
javascript
let form = new FormData();
form.append('username', 'DDD');
form.append('password', '987654321');
for (let item of form.entries()) {
console.log(item[0] + ":" + item[1]);
}
The FormData.entries() 方法返回一个 iterator对象,此对象可以遍历访问 FormData 中的键值对。其中键值对的 key 是一个 USVString 对象;value 是一个 USVString , 或者 Blob对象。
数组的展开运算符内部使用了迭代器
这里要注意一点,对象的展开({...{a:'1', b:'2'}}
)不是通过Iterator
实现的
javascript
console.log(...'string');
console.log(...[9,2,3]);
console.log(...new Set([1,8,9]));
数组的解构赋值内部使用了迭代器
javascript
const [a, b] = [1,2];
const [c, d] = [...[1,2]];
const [e, f] = "hi";
const [g, h] = [..."hi"];
const [i, j] = new Set([3, 4]);
const [k, l] = [...new Set([3, 4])];
Set和Map的构造函数内部使用了迭代器
javascript
new Set([1,2,3,4])
new Map([[0, 1],[1, 2]])