[TOC]
什么是iterator
iterator是一种接口机制,为各种不同的数据结构提供统一的访问机制
Iterator的作用:
- 为各种数据结构,提供一个统一的、简便的访问接口;
- 使得数据结构的成员能够按某种次序排列;
- ES6创造了一种新的遍历命令for...of循环,Iterator接口主要供for...of消费。
Iterator的工作原理:
- 创建一个指针对象,指向当前数据结构的起始位置。也就是说,遍历器对象本质上就是一个指针对象。
- 第一次调用指针对象的next方法,指针自动指向数据结构的第一个成员。
- 接下来不断调用next方法,指针会一直往后移动,直到指向最后一个成员。
- 每调用next方法返回的是一个包含value和done的对象,{value: 当前成员的值, done: 布尔值} 4.1 value表示当前成员的值,done对应的布尔值表示当前的数据结构是否遍历结束。 4.2 当遍历结束的时候返回的value值是undefined,done值为true。
原生具备iterator接口的数据类型
- Array
- arguments
- set/map
- String
来看个🌰:
const arr = [1, 2, 3];
const it = arr[Symbol.iterator]();
let res;
do {
res = it.next();
console.log(res);
} while (!res.done);
2
3
4
5
6
7
8
变量arr是一个数组,原生就具有遍历器接口,部署在arr的Symbol.iterator
属性上面。所以,调用这个属性,就得到遍历器对象。
对于原生部署Iterator接口的数据结构,不需要自己写遍历器生成函数,for...of
循环会自动遍历它们。除此之外,其他数据结构(主要是对象)的Iterator接口,都需要自己在Symbol.iterator属性上面部署,这样才会被for...of循环遍历。
需要注意:对象是没有部署iterator接口的。
ES6方法和iterator接口的关系
- 使用解构赋值以及
...
三点运算符时会调用iterator接口; - Generator函数的yeild语句会调用iterator接口
自定义iterator接口(遍历器对象)
function makeIterator(array) {
let nextIndex = 0; // 记录访问数据结构的位置
return { // 返回一个遍历器对象
next() {
const done = nextIndex === array.length;
const value = done ? undefined : array[nextIndex++];
return {
value,
done
};
}
};
}
const it = makeIterator(['a', 'b']);
let result;
do {
result= it.next();
console.log(result);
} while (!result.done);
/*
{ value: 'a', done: false }
{ value: 'b', done: false }
{ value: undefined, done: true }
*/
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
遍历器对象的根本特征:
- 具有next方法;
- 每次调用next方法,都会返回一个代表包含value和done两个属性的对象。
const makeIterator = arr => {
let nextIndex = 0;
return {
next() {
const done = nextIndex >= arr.length;
const value = done ? undefined : arr[nextIndex++];
return {
value,
done
}
}
};
};
const arr = [1, 2, 3];
const it = makeIterator(arr);
let res;
do {
res = it.next();
console.log(res);
} while (!res.done);
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
{ value: 1, done: false }
{ value: 2, done: false }
{ value: 3, done: false }
{ value: undefined, done: true }
2
3
4
给对象部署iterator接口
一种数据结构只要部署了Iterator接口,我们就称这种数据结构是可遍历的(iterable)。ES6规定,默认的Iterator接口部署在数据结构的Symbol.iterator
属性上。
const obj = {
hobbies: ['🏀', '⚽️', '乒乓球'],
[Symbol.iterator]() {
const self = this;
let nextIndex = 0;
return {
next() {
const done = nextIndex >= self.hobbies.length;
const value = done ? undefined : self.hobbies[nextIndex++];
return {
value,
done
};
}
};
}
};
for (let i of obj) {
console.log(i);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
输出如下:
🏀
⚽️
乒乓球
2
3
调用 Iterator 接口的场合
解构赋值
// 对数组和Set结构进行解构赋值时,会默认调用Symbol.iterator方法。
const set = new Set().add(1).add(2).add(3);
console.log(set); // Set { 1, 2, 3 }
const [a, b] = set;
console.log(a, b); // 1 2
const [frist, ...rest] = set;
console.log(frist, rest); // 1 [ 2, 3 ]
2
3
4
5
6
7
8
9
扩展运算符
扩展运算符(...)也会调用默认的Iterator接口。
const str = 'hello';
console.log([...str]); // [ 'h', 'e', 'l', 'l', 'o' ]
const arr = [1, 2];
console.log([3, ...arr, 4]); // [ 3, 1, 2, 4 ]
2
3
4
5
只要某个数据结构(比如说类数组和字符串)部署了Iterator接口,就可以对它使用扩展运算符,将其转为数组。
yield*
yield*
后面跟的是一个可遍历的结构,它会调用该结构的遍历器接口。
const generator = function* () {
yield 1;
yield* [2,3,4];
yield 5;
};
const it = generator();
it.next(); // { value: 1, done: false }
it.next(); // { value: 2, done: false }
it.next(); // { value: 3, done: false }
it.next(); // { value: 4, done: false }
it.next(); // { value: 5, done: false }
it.next(); // { value: undefined, done: true }
2
3
4
5
6
7
8
9
10
11
12
13
14
其他场合
由于数组的遍历会调用遍历器接口,所以任何接受数组作为参数的场合,其实都调用了遍历器接口。下面是一些例子。
- for...of
- Array.from()
- Map(), Set(), WeakMap(), WeakSet()(比如new Map([['a',1],['b',2]]))
- Promise.all()
- Promise.race()
字符串的Iterator接口
const str = 'hello';
// console.log(typeof str[Symbol.iterator]); // 'function'
const it = str[Symbol.iterator]();
let res;
do {
res = it.next();
console.log(res);
} while (!res.done);
2
3
4
5
6
7
8
9
调用字符串的Symbol.iterator方法返回一个遍历器对象,在这个遍历器上可以调用next方法,实现对于字符串的遍历。
{ value: 'h', done: false }
{ value: 'e', done: false }
{ value: 'l', done: false }
{ value: 'l', done: false }
{ value: 'o', done: false }
{ value: undefined, done: true }
2
3
4
5
6
const str = 'hello';
const proto = Object.getPrototypeOf(str);
// 覆盖原生的Symbol.iterator方法,达到修改遍历器行为的目的
proto[Symbol.iterator] = function() {
return {
_frist: true,
next() {
if (this._frist) {
this._frist = false;
return {
value: 'bye',
done: false
};
} else {
return {
value: undefined,
done: true
};
}
}
};
}
console.log([...str]); // [ 'bye' ]
console.log(str); // hello
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
上述代码中,字符串str的Symbol.iterator方法被修改了,所以扩展运算符(...)返回的值变成了bye,而字符串本身还是hello。
Iterator 接口与 Generator函数
Generator函数返回一个遍历器对象,yield命令给出每一步的返回值,是Symbol.iterator方法的最简单实现。
const obj = {
[Symbol.iterator]: function* () {
yield 1;
yield 2;
yield 3;
}
};
// 或者简写成下面的形式
const obj = {
* [Symbol.iterator]() {
yield 1;
yield 2;
yield 33;
}
};
console.log([...obj]); // [ 1, 2, 3 ]
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
遍历器对象的return和throw方法
const arr = [1, 2, 3];
for (let i of arr) {
console.log(i); // 只会输出1
break;
}
2
3
4
5
return方法(必须返回一个对象)的使用场合是:for...of循环提前退出,就会调用return方法。for...of循环提前退出的原因主要有两个:
- 代码出错
- 使用了break语句
throw方法主要是配合 Generator函数使用,一般的遍历器对象用不到这个方法。
for...of
- 遍历数组
- 遍历Set
- 遍历Map
- 遍历字符串
- 遍历伪数组
for...of
循环不能直接遍历对象,可以采用下面两种方法:
const obj = {
name: 'lisi',
age: 12
};
for (let key of Object.keys(obj)) {
console.log(key, obj[key]);
}
2
3
4
5
6
7
8
const obj = {
name: 'lisi',
age: 12
};
// 使用Generator函数将对象重新包装一下
function* entries(obj) {
for (let key of Object.keys(obj)) {
yield [key, obj[key]];
}
}
for (let [key, value] of entries(obj)) {
console.log(key, value);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
与其他遍历语法的比较
以数组为例,js提供多种遍历语法。最原始的写法就是for循环。
for (var index = 0; index < myArray.length; index++) {
console.log(myArray[index]);
}
2
3
for循环缺点:写法比较麻烦,可读性差。
arr.forEach(value => {
if (value === 1) {
break; // 报错 SyntaxError: Illegal break statement
}
console.log(value);
});
2
3
4
5
6
forEach缺点:无法中途跳出forEach循环,break命令或return命令都不生效。
for...in循环可以遍历数组的键名。
for (var index in arr) {
console.log(arr[index]);
}
2
3
// 原型上的元素也会被for...in遍历
Array.prototype.age = 12;
const hobbies = ['🏀', '⚽️', '🏉'];
// 手动添加的元素也会被for...in遍历
hobbies.name = 'test';
for (const key in hobbies) {
// console.log(key); // 0 1 2
// console.log(typeof key); // string
console.log(hobbies[key]);
}
2
3
4
5
6
7
8
9
10
11
🏀
⚽️
🏉
test
12
2
3
4
5
for...in循环有几个缺点:
- 数组的键名是数字,但是for...in循环是以字符串作为键名'0'、'1'、'2'。
- for...in循环不仅遍历数字键名,还会遍历手动添加的其他键,甚至包括原型链上的键。
- 某些情况下,for...in循环会以任意顺序遍历键名。
总之,for...in循环主要是为遍历对象而设计的,不适用于遍历数组。
for (let value of arr) {
console.log(value);
}
2
3
// 原型上的元素不会被for...of遍历
Array.prototype.age = 12;
const hobbies = ['🏀', '⚽️', '🏉'];
// 手动添加的元素不会被for...of遍历
hobbies.name = 'test';
for (const key of hobbies) {
// key就是数组元素本身而不是索引值
console.log(key); // '🏀' '⚽️' '🏉'
}
2
3
4
5
6
7
8
9
10
for...of循环相比上面几种做法,有一些显著的优点:
- 和
for...in
一样的简洁语法,但是没有for...in
那些缺点。 - 不同于forEach方法,for...of循环可以与break(跳出当前循环)、continue(跳过本次循环)和return配合使用。
- 提供了遍历所有数据结构的统一操作接口。
for..of遍历NodeList
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
<style>
.complated {
text-decoration: line-through;
}
</style>
</head>
<body>
<ul>
<li>篮球</li>
<li>足球</li>
<li>乒乓球</li>
</ul>
<script>
const liList = document.querySelectorAll('li');
// console.log(liList);
for (const li of liList) {
li.addEventListener('click', function() {
this.classList.toggle('complated');
});
}
</script>
</body>
</html>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30