update: 2022.02.06 关于 for…of 的问题,详见最后
迭代,iteration,意思是「重复」或者「再来」。在计算机领域,我们可以简单理解为按照顺序反复的执行某一段代码。
for 与 for-of
我们来看一个关于迭代最简单的例子:
const arr = [1, 2, 3, 4];
for(let i = 0; i < arr.length; i++) {
console.log(arr[i]);
}
上面的代码就是一次通过 for 循环实现的迭代,最终遍历了一遍数组的并打印出数组中的值。
在这个例子中,我们之所以通过 for 循环遍历了一个数组,是因为我们知道了需要迭代的数据的数据结构是一个数组,并且我们可以轻松的知道迭代终止的条件,也可以手动的获取到数组的值。
for 循环的问题以及解决方案
接着我们分析一下,使用 for 循环进行迭代的缺点。
- 无法兼容兼容不同的数据结构
在上面的例子中,我们使用 for 循环对一个数组进行迭代,那如果此时将数组换成 Map,或者Set 之类的数据结构呢?我们就要重新编写一套迭代逻辑。
- 过于关注如何迭代的逻辑
使用 for 循环进行迭代,我们必须要关心起始/终止条件,而如果没有特殊的需求,大部分的迭代所做的事情是按照一定的顺序遍历一遍可迭代对象中的成员。
那么,为了解决上述问题,JavaScript 是否提供了某种方法,让我们有更加简单且高效的方式进行迭代呢?
有的, JavaScript 提供的 for...of
语句可以帮助我们解决上面使用 for 循环带来的问题。
const arr = [1, 2, 3, 4];
const map = new Map([
["key1", 1],
["key2", 2],
["key3", 3],
]);
for (item of arr) {
console.log(item);
}
// 结果如下
// 1
// 2
// 3
// 4
for (item of map) {
console.log(item);
}
// 结果如下
// ['key1', 1]
// ['key2', 2]
// ['key3', 3]
通过 for...of
语句,我们不在需要关注如何迭代,也不需要为 Map
这样的非数组数据结构兼容。for...of
提供了一个通用的方法,来解决迭代的问题。
深入 for-of 与 iterable 接口
你可能会好奇,for...of
语句为什么可以进行上面的操作。难不成有黑魔法?
其实并不是,JavaScript 为一些内建的数据结构提供了一个名为 iterable 的接口,且接口实现了迭代协议。而 for...of
会自动调用该接口,并通过该接口实现迭代。
迭代协议后文会解释,目前不理解并不影响。
凡是可以使用 for...of
进行迭代的数据结构,一定包含 iterable 接口,并且实现了 iterable 接口。
那如何理解包含 iterable 接口,和实现了 iterable 接口呢?
- 包含 Iterable 接口
实际上,要进行迭代的“东西”,依然是一个对象。无论是 Array,还是一个 Map,本质上都是对象。而所谓包含 Iterable 接口,就是有一个对象,其本身或者原型链上有一个名为 Symbol.iterator
的属性。
const arr = [1, 2];
// 此时,没有实现 iterable 接口
arr[Symbol.iterator] = null;
- 实现了 iterable 接口
所谓实现 iterable 接口,其实就是实现一个函数,该函数必须遵守某种规范(迭代协议规定的规范)。并且设置 Symbol.iterator
属性指向该函数。
const arr = [1, 2];
const iter = function(){
// 函数体内容符合迭代协议
}
// 让 arr[Symbol.iteror] 属性指向函数 iter
arr[Symbol.iterator] = iter;
所以,Map,Array 等内建的数据结构之所以可以使用 for...of
是因为这些数据结构包含了 iterable 接口,并且 JavaScript 帮我们实现了 iterable 接口。
JavaScript 并没有为所有的数据结构提供迭代的接口。下面是部分实现了该接口的数据结构
- Array
- TypedArray
- String
- Map
- Set
- arguments 对象
验证内建数据结构的 iterable 接口
JavaScript 中有许多的内建数据结构实现了 iterable 接口,接下来让我们看一看,验证一下。
const arr = [1, 2];
const str = "123";
const map = new Map([["key1", 1]]);
console.log(arr[Symbol.iterator]); // values() { [native code] }
console.log(str[Symbol.iterator]); // [Symbol.iterator]() { [native code] }
console.log(map[Symbol.iterator]); // entries() { [native code] }
我们可以看到,这些对象上面都有 Symbol.iterator
属性,并且属性值指向一个函数。
接下来我们验证一下 for...of
会自动调用 iterable 接口的说法。
// 变量沿用上面的例子
// for...of 自动调用了 iterable 接口
for(item of arr) {
console.log(item);
}
// 1
// 2
// for...of 手动调用了 iterable 接口
for(item of arr[Symbol.iterator]()) {
console.log(item);
}
// 1
// 2
最后再来看一看如果我们不实现 iterable 接口时,使用 for...of
语句会发生什么
const arr = [1, 2];
arr[Symbol.iterator] = null;
for(const item of arr) {
console.log(item);
}
// Uncaught TypeError: arr is not iterable
小结
- 为了解决使用 for 循环迭代的缺点,JavaScript 引入了
for...of
语句,该语句会自动调用 iterable 接口。 - 对象自身或者原型链上存在
Symbol.iterator
属性就算是有 iterable 接口。 Symbol.iterator
属性所引用的值是一个函数,该函数需要符合迭代协议。
接下来,我们探究一下迭代协议,从根本上理解,JavaScript 是如何实现迭代的。
迭代协议
首先,你要理解「协议」这个词。协议是人们提前约定好的一种行为规范,通过遵守这个协议达到某一些目的。
比如说语言不通的人要如何进行交流呢?没错,我们可以提前约定人们使用中文进行交流。那么换成「计算机」一点的说法就是,我们制定了一个人类交流协议,并且通过使用中文交流来实现这个协议。如果人们想要相互交流,就必须要使用人类交流协议,不然双方无法理解对方的意思。
那么,换成迭代协议,简单理解就是,在 JavaScript 中,如果我们想要进行迭代操作,那么就必须要遵守迭代协议,不然 JavaScript 就无法理解我们的意思。
而我们上文提及的 iterable 接口就是严格遵守迭代协议的,或者说 iterable 接口实现了迭代协议。
可迭代协议
可迭代协议规定了什么样的对象,可以进行迭代。
那么,可迭代协议具体是如何规定的呢?
- 若 JavaScript 对象可迭代,则称其为可迭代对象
- 可迭代对象(或原型链)上必须有一个属性
@@iterator
,且属性值为一个无参数的函数,其返回值为一个符合迭代器协议的对象。也就是定义了一个工厂函数。
可以使用
@@well-know symbol name
的方式去代表一个 well-know Symbol。这里是用@@iterator
表示Symbol.iterator
reference:https://262.ecma-international.org/6.0/#sec-well-known-symbols
我们用代码来模拟实现一下可迭代协议:
// 一个对象,我们定义该对象
const obj = {};
const iteratorFunc = function(){
// 返回一个符合迭代器协议的对象 => 也就是迭代器对象
return iteratorObject;
}
obj[Symbol.iterator] = iteratorFunc;
我们在使用迭代的时候,并不需要显式的调用这个工厂函数来生成迭代器。以下的语言结构将会在后台调用可迭代对象的这个工厂函数,从而创建迭代器。
- for-of
- 数组解构
- 扩展操作符
- Array.from()
- 创建集合
- 创建映射
- Promise.all() 接收由 promise 组成的可迭代对象
- Promise.race() 接收由 promise 组成的可跌代对象
- yield* 操作符,在生成器中使用
迭代器协议
迭代器协议规定了产生一系列值的标准方式。这一系列值可能是是无限的,也可能是有限的。若一系列值是有限的,则在迭代完毕之后返回一个默认值。
接下来,我们看一下迭代器协议的具体规定:
- 迭代器是一个对象
- 该对象必须实现一个
next()
方法,且约束如下- 该方法,有一个参数,或者无参数
- 该方法,必须返回一个对象
- 返回的对象中,有如下属性
- done,一个布尔值,用以表示是否还需要迭代
- value,用于表示当前迭代到的值,当
done: true
时,可以省略
从上面我们知道,迭代器是一个对象。那么换句话说,一个遵守迭代器协议的对象被称为迭代器。
下面,我们用代码来实现一个简单的迭代器
// 迭代器是一个对象
const iteratorObject = {};
// 实现 next() 方法
iteratorObject.next = function() {
// 返回一个类型为 {done: boolean, value: any} 的对象
return {
done: true,
value: 'iteration done'
};
}
迭代器运作原理
迭代器是一种一次性使用的对象,用于迭代与其关联的可迭代对象。迭代器对象中有一个 next()
方法,每一次调用 next()
方法都会返回一个结果对象,结果对象将会包含两个属性:done 和 value。done 是一个布尔值,表示是否还可以再次调用 next()
取得下一个值,value 包含着可迭代对象的值。
我们直接来看例子:
const arr = [1, 2, 3];
const iter = arr[Symbol.iterator]();
console.log(iter.next()); // {value: 1, done: false}
console.log(iter.next()); // {value: 2, done: false}
console.log(iter.next()); // {value: 3, done: false}
console.log(iter.next()); // {value: undefined, done: false}
其中有几个需要注意的点:
- 每一个迭代器都代表对可迭代对象的一次性有序遍历。不同的迭代器实例没有关联
- 迭代器并不与可迭代对象的某一个时刻的快照绑定
- 迭代器维护着一个指向可迭代对象的引用,因此迭代器会组织垃圾回收。
提前终止迭代器
for...of
语句和 for
语句类型,也可以使用 bread
、continue
、return
或者throw
提前退出。
我们可以在迭代器对象中实现一个 return()
方法,该方法会在迭代器提前关闭的时候执行。
// 迭代器是一个对象
const iteratorObject = {
next() {
// 返回一个类型为 {done: boolean, value: any} 的对象
return {
done: true,
value: "iteration done",
};
},
return() {
console.log("提前终止");
return {
done: true,
};
},
};
试一试
手动实现一个 iterable 接口
我们已经知道了,所谓的 iterable 接口,就是迭代协议的实现。而迭代协议分为两个部分:可迭代协议,迭代器协议,因此我们先实现可迭代协议。
我们将手动实现一个 Array 的 iterable 接口,不同的数据类型 iterable 接口的实现是不一样的。
- 实现可迭代协议 ```javascript const arr = [1, 2, 3];
function iter() { // 返回一个符合迭代器协议的对象 return iteratorObj; }
// 一个名为 Symbol.iterator 的属性,并且指向一个无参数的函数,该函数返回一个符合迭代器协议的对象 arr[Symbol.iterator] = iter;
2. 实现迭代器协议
```javascript
const arr = [1, 2, 3];
function iter() {
// 做一个闭包,储存索引
let i = 0;
// this指向的是调用该函数的对象,此时调用该函数的对象就是 arr 这个数组
let arr = this;
// 返回一个符合迭代器协议的对象,该对象包含一个 next 函数;
return {
next: function () {
if (i >= arr.length) {
return { done: true };
}
console.log('自定义迭代');
return { done: false, value: arr[i++] };
},
};
}
// 一个名为 Symbol.iterator 的属性,并且指向一个无参数的函数,该函数返回一个符合迭代器协议的对象
arr[Symbol.iterator] = iter;
下面我们用 for...of
语句验证一下
for(const item of arr) {
console.log(item);
}
// 自定义迭代
// 1
// 自定义迭代
// 2
// 自定义迭代
// 3
那现在我们就手动实现了一个 iterable 接口,而不是调用 JavaScript 实现的 iterable 接口。
模拟一个 for…of
通过迭代器对象的原理,我们可以知道,所谓迭代就是不停的调用迭代器对象中的 next()
方法,直到迭代器对象中 next
方法生成的结果对象中 done 属性等于 false 的时候结束迭代。
function forOf(obj, callback) {
let iterator = null;
let result = null;
if (typeof obj[Symbol.iterator] !== "function")
throw new TypeError(result + " is not iterable");
if (typeof cb !== "function") throw new TypeError("cb must be callable");
iterator = obj[Symbol.iterator]();
result = iterator.next();
while (!result.done) {
callback(obj);
result = iterator.next();
}
}
update:
以下情况将无法在 for...of
中使用自己定义的迭代器
Array.prototype.mineKeys = function () {
const len = this.length
let i = 0
return {
next() {
if(i >= len) {
return { done: true }
}
return { value: i++, done: false }
}
}
}
const keysArr = [1, 2, 3]
// TypeError: keysArr.mineKeys is not a function or its return value is not iterable
for (const key of keysArr.mineKeys()) {
console.log(key)
}
甚至,我们在代码中使用 next
的预期都是正确是,但是放在 for...of
里面就是不对。
Array.prototype.mineKeys = function () {
const len = this.length
let i = 0
return {
next() {
if(i >= len) {
return { done: true }
}
return { value: i++, done: false }
}
}
}
const keysArr = [1, 2, 3]
const iter = keysArr.mineKeys()
console.log(iter.next())
console.log(iter.next())
console.log(iter.next())
自定义的迭代器必须在 [Symbol.iterator]
中使用
const arr = [1, 2, 3]
arr[Symbol.iterator] = function() {
const len = this.length
let i = 0
return {
next() {
if(i >= len) {
return { done: true }
}
return { value: i++, done: false }
}
}
}
for (const key of arr) {
console.log(key)
}
如果需要自定义 Array.prototype.keys
这种方法,可以使用生成器。
Array.prototype.mineEntries = function * () {
const arr = this
for (let i = 0; i < arr.length; i++) {
yield [i, arr[i]]
}
}
const entriesArr = [1, 2, 3]
for (const [key, value] of entriesArr.mineEntries()) {
console.log(key, value)
}