[TOC]

什么是iterator

iterator是一种接口机制,为各种不同的数据结构提供统一的访问机制

Iterator的作用:

  1. 为各种数据结构,提供一个统一的、简便的访问接口;
  2. 使得数据结构的成员能够按某种次序排列;
  3. ES6创造了一种新的遍历命令for...of循环,Iterator接口主要供for...of消费

Iterator的工作原理:

  1. 创建一个指针对象,指向当前数据结构的起始位置。也就是说,遍历器对象本质上就是一个指针对象。
  2. 第一次调用指针对象的next方法,指针自动指向数据结构的第一个成员。
  3. 接下来不断调用next方法,指针会一直往后移动,直到指向最后一个成员。
  4. 每调用next方法返回的是一个包含value和done的对象,{value: 当前成员的值, done: 布尔值} 4.1 value表示当前成员的值,done对应的布尔值表示当前的数据结构是否遍历结束。 4.2 当遍历结束的时候返回的value值是undefined,done值为true。

原生具备iterator接口的数据类型

  1. Array
  2. arguments
  3. set/map
  4. String

来看个🌰:

const arr = [1, 2, 3];
const it = arr[Symbol.iterator]();

let res;
do {
    res = it.next();
    console.log(res);
} while (!res.done);
1
2
3
4
5
6
7
8

变量arr是一个数组,原生就具有遍历器接口,部署在arr的Symbol.iterator属性上面。所以,调用这个属性,就得到遍历器对象。

对于原生部署Iterator接口的数据结构,不需要自己写遍历器生成函数,for...of循环会自动遍历它们。除此之外,其他数据结构(主要是对象)的Iterator接口,都需要自己在Symbol.iterator属性上面部署,这样才会被for...of循环遍历。

需要注意:对象是没有部署iterator接口的。

ES6方法和iterator接口的关系

  1. 使用解构赋值以及...三点运算符时会调用iterator接口;
  2. 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 }
*/
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25

遍历器对象的根本特征:

  1. 具有next方法;
  2. 每次调用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);
1
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 }
1
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);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

输出如下:

🏀
⚽️
乒乓球
1
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 ]
1
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 ]
1
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 }
1
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);
1
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 }
1
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
1
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 ]
1
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;
}
1
2
3
4
5

return方法(必须返回一个对象)的使用场合是:for...of循环提前退出,就会调用return方法。for...of循环提前退出的原因主要有两个:

  • 代码出错
  • 使用了break语句

throw方法主要是配合 Generator函数使用,一般的遍历器对象用不到这个方法。

for...of

  1. 遍历数组
  2. 遍历Set
  3. 遍历Map
  4. 遍历字符串
  5. 遍历伪数组

for...of循环不能直接遍历对象,可以采用下面两种方法:

const obj = {
    name: 'lisi',
    age: 12
};

for (let key of Object.keys(obj)) {
    console.log(key, obj[key]);
}
1
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);
}
1
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]);
}
1
2
3

for循环缺点:写法比较麻烦,可读性差。

arr.forEach(value => {
    if (value === 1) {
        break; // 报错 SyntaxError: Illegal break statement
    }
    console.log(value);
});
1
2
3
4
5
6

forEach缺点:无法中途跳出forEach循环,break命令或return命令都不生效。

for...in循环可以遍历数组的键名。

for (var index in arr) {
  console.log(arr[index]);
}
1
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]);
}
1
2
3
4
5
6
7
8
9
10
11
🏀
⚽️
🏉
test
12
1
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);
}
1
2
3
// 原型上的元素不会被for...of遍历
Array.prototype.age = 12;
const hobbies = ['🏀', '⚽️', '🏉'];

// 手动添加的元素不会被for...of遍历
hobbies.name = 'test';
for (const key of hobbies) {
    // key就是数组元素本身而不是索引值
    console.log(key); // '🏀' '⚽️' '🏉'
}
1
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>
1
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