一、遍历器 Iterator
1. 什么是遍历器
- 遍历器(Iterator)是一种接口,
为各种不同的数据结构提供统一的访问机制
- 任何数据结构只要部署
Iterator 接口
,就可以完成遍历操作,称可遍历的
(iterable) - Iterator 接口主要供 ES6
for...of
使用。当使用 for...of 循环遍历某种数据结构时,该循环会自动去寻找 Iterator 接口。
2. 遍历器接口怎么实现的
ES6 规定默认 Iterator 接口部署在 Symbol.iterator
属性上,它是一个函数,返回一个遍历器对象
。
遍历器对象
特征: 具有 next 方法,每次调用 next 方法都会返回一个代表当前成员的信息对象,具有 value 和 done 两个属性。
关系:实现 Iterator 接口 -> 部署 Symbol.iterator 属性 -> 该方法返回遍历器对象 -> 可遍历的 -> for...of 遍历
ES6 还提供了 11 个
内置的 Symbol 值,指向语言内部使用的方法来实现
。ES6 之前并没有暴露给开发者。Symbol.iterator 属性就是一个 js 内置的、类型为 Symbol 的特殊值,指向该对象的默认遍历器方法
- 自定义 Iterator 接口的例子
let obj = {
[Symbol.iterator]: function () {
let i = 0;
return {
next: function () {
if (i < 3) {
i++;
return {
value: i,
done: false,
};
} else {
return {
value: undefined,
done: true,
};
}
},
};
},
};
for (let i of obj) {
console.log(i); // 1 2 3 (注意done为true时,for循环终止,不会获取本次return结果!)
}
- 模拟遍历器的 next 操作过程,来遍历数组
function makeIterator(array) {
let nextIndex = 0;
return {
next: function () {
return nextIndex < array.length
? { value: array[nextIndex++], done: false }
: { value: undefined, done: true };
},
};
}
let it = makeIterator(["a", "b"]);
it.next(); // { value: "a", done: false }
it.next(); // { value: "b", done: false }
it.next(); // { value: undefined, done: true }
但是此时不可以 for..of 遍历,因为并不是部署在Symbol.iterator
属性上的。
3. 简单了解:迭代协议
- 可迭代协议: 允许 JS 对象定义它们的迭代行为,即成为可迭代对象
- 可迭代对象: 必须实现 @@iterator 方法, 可通过常量 Symbol.iterator 访问
- 迭代器协议: 实现了一个拥有以下语义的 next() 方法
- next()方法: 返回一个拥有 value、done 属性的对象
@@符号,JS 中通过 Symbol 构造函数公开,例如,@@toPrimitive 公开为 Symbol.toPrimitive
二、具备 Iterator 接口的数据结构
- ES6 的有些数据结构原生具备 Iterator 接口(部署了 Symbol.iterator 属性)返回一个遍历器对象
- 即不用任何处理,就可以被 for...of 循环遍历
- 但有一些数据结构没有部署(比如对象)
原生具备 Iterator 接口的数据结构:
- Array
- String
- Map
- Set
- TypedArray
- 类似数组的对象:函数的 arguments 对象、NodeList 对象
- 生成器 Generator
1. Array.prototype[Symbol.iterator]()
- 数组原生具有遍历器接口,部署在 Symbol.iterator 属性上。调用这个属性就得到遍历器对象
- 数组的遍历器接口只返回具有数字索引的属性
let arr = ["a", "b", "c"];
let it = arr[Symbol.iterator](); // Array Iterator {}
it.next(); // { value: 'a', done: false }
Array.prototype[Symbol.iterator](); // 返回遍历器对象 Array Iterator {}
// 只返回具有数字索引的属性
let arr = [3, 5, 7];
arr.foo = "hello";
for (let i of arr) {
console.log(i); // "3", "5", "7"
}
- ES6 新增的方法,也会返回 Array Iterator 对象
Array.prototype.keys()
对键名的遍历
Array.prototype.values()
对键值的遍历
Array.prototype.entries()
对键值对的遍历
let array1 = ["a", "b", "c"];
let iterator = array1.entries();
for (let [key, value] of iterator) {
console.log([key, value]);
// [0,'a']
// [1,'b']
// [2,'c']
}
// 也可以手动遍历
iterator.next();
2. String.prototype[Symbol.iterator]()
字符串也是一个类似数组的对象,也原生具有 Iterator 接口。 String 的默认迭代器会依次返回该字符串的各码点(code point)
let someString = "hi";
let i = someString[Symbol.iterator](); // String Iterator {}
i.next(); // {value:'h', done:false}
i.next(); // {value:'i', done:true}
i.next(); // {value:undefined, done:true}
例子:覆盖原生的 Symbol.iterator 方法、修改遍历器行为
let str = new String("hi");
[...str]; // ["h", "i"]
// 重新定义的 Symbol.iterator 方法,会影响原来内置语法结构的行为!
str[Symbol.iterator] = function () {
return {
// 只返回一次字符串 "bye"
next: function () {
if (this._first) {
this._first = false;
return { value: "bye", done: false };
} else {
return { done: true };
}
},
_first: true,
};
};
[...str]; // ["bye"]
3. 类似数组的对象
- DOM NodeList 对象 是类似数组的对象,具有遍历接口,可以直接遍历
函数的 arguments 对象
也是类似数组的对象,具有遍历接口,可以直接遍历- 对于其他
类似数组的对象
,若想部署 Iterator 接口有一个简便方法,就是 Symbol.iterator 方法直接引用数组的 Symbol.iterator 方法
。但普通对象 部署 数组的Symbol.iterator 方法,并无效果
。
// 1. 函数的 arguments 对象
(function () {
for (let argument of arguments) {
console.log(argument); // 1 2 3
}
})(1, 2, 3);
// 2. nodeList 对象
let nodeList = document.querySelectorAll("div");
for (let item of nodeList) {
console.log(item); // 打印页面的div元素
}
// 3. array like object
let arrLikeObj = {
0: "aaa",
1: "bbb",
length: 2,
};
for (let item of arrLikeObj) {
console.log("item:", item); // 报错 : arrLikeObj is not iterable
}
// 4. 部署 Iterator 接口: 1)设置 Symbol.iterator方法 2)Array.from方法将其转为数组
arrLikeObj[Symbol.iterator] = Array.prototype[Symbol.iterator];
for (let item of arrLikeObj) {
console.log(item); // aaa bbb
}
// 或者
for (let item of Array.from(arrLikeObj)) {
console.log(item); // aaa bbb
}
4. 对象(原生没有部署遍历器)
- 对于对象这种没有部署 Iterator 接口的数据结构,都需要自己在 Symbol.iterator 属性上面部署,这样才会被 for...of 循环遍历。
let obj = {
data: ["hello", "world"],
[Symbol.iterator]() {
const self = this;
let index = 0;
return {
next() {
if (index < self.data.length) {
return {
value: self.data[index++],
done: false,
};
} else {
return { value: undefined, done: true };
}
},
};
},
};
for (let item of obj) {
console.log(item); // "hello" "world"
}
- 对象为什么没有部署遍历器接口?
思考原因如下:
- 使用 for...of 来进行对象的默认行为的遍历,要考虑的情况很多,比如 own property 还是原型上属性、是否可枚举、遍历 key/value 不同组合形式等,很难定义默认行为
- ES6 原生提供了 Map 结构,完善了对象的用法,也可以使用
keys()/values()/entries()
完成各种形式的遍历。Map 的遍历顺序就是插入顺序。
所以如何实现对象的遍历,给我们的可用场景其实很多: ↓↓↓
for (const k of Object.keys(obj)) ... // enumerable own keys
for (const [k, v] of Object.entries(obj)) ... // enumerable own [key, value]
for (const k of Object.getOwnPropertyNames(obj)) // all own keys
for (const s of Object.getOwnPropertySymbols(obj)) // all own symbols
for (const k of Reflect.ownKeys(obj)) // all own keys (include symbo
Why are Objects not Iterable in JavaScript?
5. Map 和 Set 结构
- Set 和 Map 的实例(new Map()),原生具有 Iterator 接口
- 遍历的顺序是按照各个成员被添加进数据结构的顺序
- Map 和 Set 的 3 个实例方法,也都返回遍历器,都可以使用 for...of 遍历
- Map.prototype.keys()/values()/entries()
- Set.prototype.keys()/values()/entries()
- Set 结构遍历时,返回的是一个值;而 Map 结构遍历时,返回的是一个数组,该数组的两个成员分别为当前 Map 成员的键名和键值。
// 1. set实例
let set = new Set([1, 2, 3, 3]); // Set(3){1,2,3}
for (let i of set) {
console.log(i); // 1 2 3
}
// 实例方法返回的也是遍历器
set.values(); // SetIterator {1,2,3}
for (let i of set.values()) {
console.log(i); // 1 2 3
}
// 2. map实例
let map = new Map([
["name", 1],
["age", 2],
]); // Map(2) {"name" => 1, "age" => 2}
for (let i of map) {
console.log(i); // [name,1] [age,2]
}
// 实例方法返回的也是遍历器
map.keys(); // MapIterator {name,age}
for (let i of map.keys()) {
console.log(i); // name age
}
map.entries(); // MapIterator {"name" => 1, "age" => 2}
for (let i of map.entries()) {
console.log(i); // [name,1] [age,2] √
}
- Set 的默认遍历器接口,就是它的 values 方法;Map 的默认遍历器接口,就是它的 entries 方法。这意味着,可以省略 values / entries 方法,直接用 for...of 循环遍历 Set/ Map。
Set.prototype[Symbol.iterator] === Set.prototype.values; // true
Map.prototype[Symbol.iterator] === Map.prototype.entries; // true
6. 使用生成器 Generator
1. 基础
- Generator 函数是一个状态机,封装了多个内部状态
- 执行 Generator 函数会返回一个
遍历器对象
,可以依次遍历 Generator 函数内部的每一个状态 - 特征
- function 关键字与函数名之间有一个星号
- 函数体内部使用 yield 表达式,定义不同的内部状态(yield : “产出”)
- 调用 Generator 函数后,该函数并不执行,返回的也不是函数运行结果,而是
返回一个指向内部状态的指针对象(遍历器对象)
。必须调用遍历器对象的 next 方法,使得指针移向下一个状态
function* helloWorldGenerator() {
yield "hello";
yield "world";
return "ending";
}
let hw = helloWorldGenerator();
hw.next(); // { value: 'hello', done: false }
hw.next(); // { value: 'world', done: false }
hw.next(); // { value: 'ending', done: true }
hw.next(); // { value: undefined, done: true }
2. 与 Iterator 接口的关系
由于 Generator 函数就是遍历器生成函数(返回一个遍历器对象),因此可以把 Generator 赋值给对象的 Symbol.iterator 属性,从而使得该对象具有 Iterator 接口。
var myIterable = {};
myIterable[Symbol.iterator] = function* () {
yield 1;
yield 2;
yield 3;
};
// myIterable对象具有了 Iterator 接口,可以被...运算符遍历
[...myIterable]; // [1, 2, 3]
for...of 循环可以自动遍历 Generator 函数运行时生成的 Iterator 对象,且此时不再需要调用 next 方法。
function* foo() {
yield 1;
yield 2;
yield 3;
yield 4;
yield 5;
return 6;
}
for (let v of foo()) {
console.log(v);
}
// 1 2 3 4 5
3. 遍历器对象的 return()
,throw()
return()
方法必须返回一个对象,这是 Generator 语法决定的- 如果 for...of 循环提前退出(通常是因为出错,或者有 break 语句),就会调用 return()方法
- 如果一个对象在完成遍历前,需要清理或释放资源,就可以部署 return()方法
throw()
方法主要是配合 Generator 函数使用,一般的遍历器对象用不到这个方法。可参阅《阮一峰-Generator 函数》一章。
let obj = {
data: ["a", "b", "c", "d"],
[Symbol.iterator]() {
const self = this;
let index = 0;
return {
next() {
if (index < self.data.length) {
return {
value: self.data[index++],
done: false,
};
} else {
return { value: undefined, done: true };
}
},
return() {
console.log("done");
return { done: true };
},
};
},
};
for (let i of obj) {
console.log(i);
break;
}
三、调用 Iterator 接口的场景
有一些场合会默认调用 Iterator 接口(即 Symbol.iterator 方法)
1. 解构赋值
对 数组
和 Set 结构
进行解构赋值时,会默认调用 Symbol.iterator 方法。
let set = new Set().add("a").add("b").add("c"); // Set(3) {"a", "b", "c"}
let [x, y] = set; // x='a'; y='b'
let [first, ...rest] = set; // first='a'; rest=['b','c'];
注意对象的解构赋值不行,因为 Object 没有部署 Iterator 接口
2. 扩展运算符
任何部署了 Iterator 接口的数据结构,都可以用扩展运算符
转为真正的数组
// 字符串转数组
var str = "hello";
[...str]; // ['h','e','l','l','o']
// 类数组对象转数组
let nodeList = document.querySelectorAll("div");
[...nodeList];
// Generator 函数
const go = function* () {
yield 1;
yield 2;
yield 3;
};
[...go()]; // [1, 2, 3]
3. yield*
表达式
yield*
后面跟的是一个可遍历的结构,它会调用该结构的遍历器接口。
let generator = function* () {
yield 1;
yield* [2, 3, 4];
yield 5;
};
var iterator = generator();
iterator.next(); // { value: 1, done: false }
iterator.next(); // { value: 2, done: false }
iterator.next(); // { value: 3, done: false }
iterator.next(); // { value: 4, done: false }
iterator.next(); // { value: 5, done: false }
iterator.next(); // { value: undefined, done: true }
4. 接受数组作为参数的场合
任何接受数组作为参数的场合,其实都调用了遍历器接口(数组的遍历会调用遍历器接口)
- Array.from()
- new Map(), new Set(), new WeakMap(), new WeakSet()
- Promise.all()、Promise.race()
- for...of
1. Array.from()
部署了 Iterator 接口的数据结构,Array.from 都能将其转为数组
Array.from("hello");
// ['h', 'e', 'l', 'l', 'o']
let namesSet = new Set(["a", "b"]);
Array.from(namesSet); // ['a', 'b']
2. for...of
for...of 循环内部调用的是数据结构的 Symbol.iterator 方法
- 特点
- 可以使用
break
,continue
或return
终止循环 - 生成器不应该重用。在退出循环后,生成器关闭,并尝试再次迭代,不会产生任何进一步的结果。
let array1 = ["a", "b", "c"];
let iterator = array1.entries();
for (let [key, value] of iterator) {
console.log([key, value]);
}
// 假如继续调用
iterator.next(); // {value: undefined, done: true}
for...of
与for...in
的区别
for...in
- 循环主要是为遍历对象而设计的,不适用于遍历数组
for...in
语句以任意顺序遍历对象的可枚举属性
。for...in
循环读取键名
for...of
循环读取键值for...of
语句遍历可迭代对象for...of
循环读取键值- 如果要通过
for...of
循环获取数组的索引,可以借助数组实例的entries 方法
和keys 方法
底层原理可参考: