0%

熟悉又陌生的 Iterator

何为熟悉:JavaScript 提供了许多迭代集合的方法,我们日常搬砖过程中消费的一些遍历方法正是利用了 Iterator 的访问机制。 亦为陌生:Iterator 在一些集合中是默默地付出的,对用户层面是不感知的,因此我们可能会忽略掉它的存在。本文笔者将带领大家一起揭开 Iterator 的神秘面纱。

Iterator 基本概念

在此之前我们简单了解下 Iterator protocol,迭代协议分为可迭代协议迭代器协议

  • 可迭代协议:允许 JavaScript 对象定义或定制它们的迭代行为。目前内置可迭代对象:String、Array、Map 和 Set 等,它们的原型对象都实现了 @@iterator 方法,通常可通过常量 Symbol.iterator 访问该属性(避免冲突:key 重复或者与用户代码冲突,所以这里使用 Symbol 唯一值)。
  • 迭代器协议:定义了产生一系列值(无论是有限个还是无限个)的标准方式。当值为有限个时,所有的值都被迭代完毕后,则会返回一个默认返回值。只有实现了一个拥有以下语义的 next() 方法(一个无参数函数,返回一个应当拥有以下两个属性的对象),一个对象才能成为迭代器:
1
2
3
4
5
6
next:function () {
return {
done: false,
value: 'test'
}
}

基于以上知识点去理解 Iterator 就更为通俗易懂,Iterator 译为迭代器,在 JavaScript 中迭代器是一个对象,更加具体地说,迭代器是通过使用 next() 方法实现 Iterator protocol 的任何一个对象。 于是我们便可以简单自定义一个迭代器对象。

1
2
3
4
5
6
const myIterator = {
next:function() {
// ...
},
[Symbol.iterator]:function() {returnthis }
}

默认的 Iterator 接口

讲到这,就离不开与 Iterator 密不可分的 for…of 循环,因为 Iterator 诞生主要是供 for…of 消费的,我们知道 for…of 循环遍历的是可迭代对象,前面可知可迭代对象必须实现 Symbol.iterator 方法,并且可迭代对象默认的 Symbol.iterator 方法通常也符合迭代器协议,从而我们可以理解为一个可迭代对象是符合 Iterator protocol 的。

在这里,我们把 Iterator 抽象为接口, 我们知道内置可迭代对象 String、Array、Map、Set 等这些不同集合的数据结构都能被 for…of 所遍历,是因为 Iterator 接口为各种不同的数据结构提供统一的访问机制。换句话说,只要一种数据结构部署了 Iterator 接口,我们就称这种数据结构是“可遍历”的,就可以完成遍历操作。

JavaScript 中原生具备 Iterator 接口的数据结构如下:

1
2
3
4
5
6
7
Array
Map
Set
String
TypedArray
arguments 对象
NodeList 对象

看一下 Array 集合的简单例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
let arr = ['a', 'b', 'c'];

for(letvalueof arr) {
console.log(value)
}

// 大致遍历过程:

let iter = arr[Symbol.iterator]();

iter.next() // { value: 'a', done: false }
iter.next() // { value: 'b', done: false }
iter.next() // { value: 'c', done: false }
iter.next() // { value: undefined, done: true }

我们发现其实 for…of 循环本质上就是调用这个接口产生的迭代器,可以通过手动给 Object (并没有默认实现 Iterator 接口的数据结构) 定义 Iterator 简单验证下。

1
2
3
4
5
6
7
let arr = ["a", "b", "c"];
const obj = {};
obj[Symbol.iterator] = arr[Symbol.iterator].bind(arr);

for (let vof obj) {
console.log(v);// 'a' 'b' 'c'
}

调用 Iterator 接口的其他场景

除了 for…of 循环,我们还有很多常见的默认调用 Iterator 接口的场景。

  • 解构赋值 (注意:目标对象为可迭代对象(Array))
1
2
3
4
5
6
7
8
9
let set =new Set(["a", "b", "c", "c"]);
let [first, ...rest] = set;
console.log(first, rest);// 'a', ['b', 'c']// 手动部署 Iterator 接口let set1 =new Set({
0: "a",
1: "b",
length: 2,
[Symbol.iterator]: Array.prototype[Symbol.iterator]
});
console.log([...set1]);// ['a', 'b']
  • 扩展运算符 (注意:目标对象为可迭代对象(Array))

    1
    2
    const str = 'hello';
    console.log([...str])// ['h','e','l','l','o']
  • yield*

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    let generator =function* () {
    yield 1;
    yield* [2, 3];
    yield 4;
    };

    const 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: undefined, done: true }
  • 其他一些场景
    由于数组的遍历会调用迭代器接口,所以任何接受数组作为参数的场合,其实都调用了迭代器接口。 Array.from(), Map(), Set(), Promise.all()

    1
    2
    3
    4
    5
    6
    7
    8
    9
    const iterable =new Set([1, 2, 2, 1]);
    for (const valueof iterable) {
    console.log(value);
    }

    const iterable =new Map([["a", 1], ["b", 2], ["c", 3]]);
    for (const entryof iterable) {
    console.log(entry);
    }

Iterator 与 Generator

我们发现 Generator 函数的返回值就是一个迭代器对象,也就是说 Generator 函数除了状态机,还是一个迭代器对象生成函数。返回的迭代器对象,可以依次遍历 Generator 函数内部的每一个状态。

于是我们可以快速通过 Generator 函数创建一个可迭代对象:

1
2
3
4
5
6
7
8

let obj = {
*[Symbol.iterator]() {
yield 1, yield 2;
}
};

console.log([...obj]); // [1,2]

疑问:

  1. 前面我们说到能被 for…of 遍历的数据结构,都必须部署 Iterator 接口,即必须实现 Symbol.iterator 方法。那么问题来啦,使用 for…of 遍历 Symbol.iterator 本身会是一个什么样的效果呢?
1
2
3
4
5
6
let str = "abc";

let strItera = str[Symbol.iterator]();
for (let valof strItera) {
console.log(val);// 'a' 'b' 'c'
}

输出结果表明:迭代器本身也会实现 Iterator 接口,也就是具有 Symbol.iterator 方法,并且这两个方法返回的是同一个迭代器 Iterator,有点像链表结构。

通过以下代码再次验证猜想正确:

1
2
3
4
5
let str = "abc";
let strItera1 = str[Symbol.iterator]();
console.log(strItera1);
let strItera2 = strItera1[Symbol.iterator]();
console.log(strItera1 === strItera2) // true
  1. 前面提到扩展运算符等也是调用 Iterator 接口,那么 Object 没有部署 Iterator 接口,为什么也能使用 … 运算符呢?

猜测:… 运算符检测到执行目标为 Object 时可能会将 Object 结构转化成已默认部署了的 Iterator 接口的数据结构。即自定义 Symbol.iterator 方法。

目前网上的一些文章并没有对扩展运算符和解构做更细致的区分,笔者这里目前理解:前面谈到的扩展运算符以及解构都是分场景的,假设目标对象为可迭代对象,即 Array 时,都是调用了可迭代对象的默认 Iterator 接口,而当目标对象为非可迭代数据结构时,即 Object 时,会调用其他方法进行数据转化。(目前暂未找到官方解释)

-------------本文结束, 感谢您的阅读-------------