ES6 中引入了for...of
、[...arr]
展开语法等很方便易用的功能,而且它们不仅仅只能用于 Array,还适用于:
- Set、Map
- String
- NodeList、HTMLCollection
我们称这些数据结构是可迭代的,那这些数据结构之间肯定存在某种共同点,即它们的原型链上都存在这一个名为Symbol.iterator
的函数:
const arr = [1, 2, 3];
console.assert(
typeof arr[Symbol.iterator] === "function",
"Array has Symbol.iterator"
);
const str = "something";
console.assert(
typeof str[Symbol.iterator] === "function",
"String also has Symbol.iterator"
);
const allButton = document.querySelectorAll("button");
console.assert(
typeof allButton[Symbol.iterator] === "function",
"NodeList has Symbol.iterator too"
);
它们都实现了Iterable
接口/协议:
interface Iterable {
[Symbol.iterator]() : Iterator;
}
interface Iterator {
next() : IteratorResult;
}
interface IteratorResult {
value: any;
done: boolean;
}
文字描述一下:“Symbol.iterator 函数需要返回一个对象,其中含有 next 函数,且这个 next 函数需要返回一个{ value: any, done: boolean }
的结构”,这里返回的对象就像是一条生产线,不过这生产线比较懒,需要一次次地问它要:“哎,给我个数据”,要的方式就是调用它提供的next()
函数,它给我们返回一个{ value: any, done: boolean }
的结构,我们从中拿到我们想要的 value 值。像上面提到的for...of
、[...arr]
展开语法也是需要这样做的,下面我们来模拟一下:
const arr = [1, 2, 3];
const iterator = arr[Symbol.iterator](); // 得到生产线
console.log(iterator.next()); // { value: 1, done: false } 第1次问生产线要数据,done为false表示我在给你生产数据
console.log(iterator.next()); // { value: 2, done: false } 第2次问生产线要数据
console.log(iterator.next()); // { value: 3, done: false } 第3次问生产线要数据
console.log(iterator.next()); // { value: undefined, done: true} done为true表示已经榨干了
console.log(iterator.next()); // { value: undefined, done: true} 榨干后再要也不给
实际上,使用for...of
、[...arr]
展开语法等操作的就是上文中提到的生产线,我们也可以直接操作它:
const arr = [1, 2, 3];
const iterator = arr[Symbol.iterator]();
for (item of iterator) {
console.log(item);
}
// 输出:
// 1
// 2
// 3
但是上面也提到了,当一条生产线被榨干之后,再要人家就不给了:
const arr = [1, 2, 3];
const iterator = arr[Symbol.iterator]();
const arrCopy = [...iterator]; // A 榨干了生产线
for (item of iterator) {
console.log(item); // B 不会被执行
}
console.log(arrCopy);
// 输出:
// [1, 2, 3]
在 A 行使用展开操作符将生产线iterator榨干了,下面再使用for...of
问人家要就肯定没有了,但是如果我们按照上面的步骤直接操作数组arr的话就能得到我们预期的结果,这是因为每次操作时都会有新的生产线出现,我们可以按照下面的步骤模拟:
const arr = [1, 2, 3];
const iterator01 = arr[Symbol.iterator]();
const arrCopy = [...iterator01]; // A 榨干了生产线 iterator01
const iterator02 = arr[Symbol.iterator](); // 新建一条生产线 iterator02
for (item of iterator02) {
console.log(item);
}
console.log(arrCopy);
// 输出:
// 1
// 2
// 3
// [1, 2, 3]
我们甚至可以分多次操作一条生产线:
const arr = [1, 2, 3, 4];
const iterator = arr[Symbol.iterator]();
let index = 0;
for (item of iterator) {
console.log(item);
if (index === 1) {
break;
}
index++;
}
const restArr = [...iterator];
console.log(restArr);
// 输出:
// 1
// 2
// [3, 4]
我们使用for...of
问生产线要了 2 次数据,然后又用展开操作符问生产线要了剩下的数据,完全可以的。
利用 Iterable 协议使普通的 Object 可迭代
上面说道,一个数据结构实现Iterable
协议了,就可以被迭代了,那我们就可以为普通的 Object 实现Iterable
协议,使其可迭代:
const symbolKey03 = Symbol("key03");
const iterableObj = {
key01: "value01",
key02: "value02",
[symbolKey03]: "value03",
[Symbol.iterator]() {
// Reflect.ownKeys获取自身所有属性,包括Symbol值作为名称的属性,但要去除特殊的Symbol.iterator
const keys = Reflect.ownKeys(this).filter((key) => key !== Symbol.iterator);
let index = 0;
return {
[Symbol.iterator]() {
return this;
},
next: () => {
// A
if (index < keys.length) {
const key = keys[index++];
return { value: [key, this[key]], done: false };
} else {
return { done: true };
}
},
};
},
};
for (let [key, value] of iterableObj) {
console.log(`${key.toString()}: ${value}`); // B
}
// 输出:
// key01: value01
// key02: value02
// Symbol(key03): value03
A 行使用箭头函数是为了继承
this
,即iterableObj,虽然箭头函数不建议在 Object 方法中使用,但非常适合在 Object 方法中的闭包中使用。
B 行显式调用
toString()
方法是因为 Symbol 类型不支持隐式转换为 String。
上面我们利用Iterable
协议使得iterableObj变成可迭代的数据,但是这样的处理很不优雅,且没有通用性,所以我们可以将上面iterableObj的实现提取出来,我们要的只是一个iterable
:
function objectEntries(obj) {
let index = 0;
const propKeys = Reflect.ownKeys(obj);
return {
[Symbol.iterator]() {
return this;
},
next() {
if (index < propKeys.length) {
const key = propKeys[index];
index++;
return { value: [key, obj[key]] };
} else {
return { done: true };
}
},
};
}
const symbolKey03 = Symbol("key03");
const obj = {
key01: "value01",
key02: "value02",
[symbolKey03]: "value03",
};
for (const [key, value] of objectEntries(obj)) {
console.log(`${key.toString()}: ${value}`);
}
// 输出:
// key01: value01
// key02: value02
// Symbol(key03): value03
上述
objectEntries
实现来自:exploringjs
同时实现 Iterator 和 Iterable
大家观察一下这个结构:
{
next: function() {
// 省略返回 { value: any, done: boolean }
},
[Symbol.iterator]() {
return this;
}
}
可以发现它既实现了Iterator
协议(有next
方法),又实现了Iterable
协议(有Symbol.iterator
方法,且返回Iterator
)。因为Iterable
的应用更广泛些,毕竟只有个next
方法的Iterator
用途不大,所以目前来看iterator
有点依附于iterable
的意思。