JavaScript iterators and generators:迭代器和生成器
随着ES6的引入,迭代器和生成器已经正式添加到JavaScript中。
迭代器能让你遍历任何符合规范的对象。在文章的第一部分,我们会探讨如何使用迭代器,以及如何让任意对象变得可迭代。
这篇博客的第二部分将完全聚焦于生成器(Generators):它们是什么、如何使用,以及在哪些场景下能派上用场。
我一向喜欢探究事物背后的运行原理。之前我写过一个系列,讲解了 JavaScript 在浏览器中是如何工作的。作为延续,本文就来深入剖析一下 JavaScript 中迭代器和生成器的底层机制。
一、什么是迭代器?
在深入理解生成器之前,我们必须先透彻掌握 JavaScript 中的迭代器,因为这两个概念是密不可分的。读完本节后,你就会明白:生成器本质上只是一种更安全、更便捷的编写迭代器的方式。
顾名思义,迭代器允许你遍历一个对象(数组本质上也是对象)。
你很可能已经用过 JavaScript 迭代器了。比如,每次你遍历数组时,其实就是在使用迭代器。除此之外,你还能遍历 Map 对象,甚至字符串。
for (let i of 'abc') {
console.log(i);
}
// Output
// "a"
// "b"
// "c"
任何实现了可迭代协议(iterable protocol)的对象,都可以通过
for...of 循环来进行遍历。如果我们往深了挖一挖,可以通过实现
@@iterator 函数(即 Symbol.iterator)来让任意对象变得可迭代,这个函数的作用是返回一个迭代器对象。二、使任何对象都可迭代
要透彻理解这一点,最好的方式就是来看一个具体的例子,演示如何让一个普通的对象变得可迭代。
我们先从一个包含用户姓名且按城市分组的对象开始:
const userNamesGroupedByLocation = {
Tokio: [
'Aiko',
'Chizu',
'Fushigi',
],
'Buenos Aires': [
'Santiago',
'Valentina',
'Lola',
],
'Saint Petersburg': [
'Sonja',
'Dunja',
'Iwan',
'Tanja',
],
};
我之所以选这个例子,是因为如果数据是这种结构,想要遍历所有用户会非常麻烦;我们通常需要写多重循环才能把所有用户都取出来。
如果我们尝试直接遍历这个对象,就会得到下面这个错误信息:
Uncaught ReferenceError: iterator is not defined
要让这个对象变得可迭代,我们首先得给它加上那个 @@iterator 函数。在代码里,我们是通过 Symbol.iterator 这个符号来访问它的。
userNamesGroupedByLocation[Symbol.iterator] = function() {
// ...
}
正如我之前提到的,迭代器函数会返回一个迭代器对象。这个对象里包含一个 next 方法,该方法在被调用时也会返回一个对象,这个对象包含两个属性:done 和 value。
userNamesGroupedByLocation[Symbol.iterator] = function() {
return {
next: () => {
return {
done: true,
value: 'hi',
};
},
};
}
这里的
value 包含了当前迭代的值,而 done 则是一个布尔值,用来告诉我们迭代是否已经结束了。在实现这个函数时,我们需要格外地小心处理
done 的值,因为如果它一直返回 false,就会导致死循环。上面的代码示例已经正确实现了可迭代协议。我们可以通过调用迭代器对象的
next 函数来测试一下:
// 调用迭代器函数返回迭代器对象
const iterator = userNamesGroupedByLocation[Symbol.iterator]();
console.log(iterator.next().value);
// "hi"
其实,用 for...of 循环来遍历对象,其底层原理就是调用 next 函数。
总结:
const userNamesGroupedByLocation = {
Tokio: [
'Aiko',
'Chizu',
'Fushigi',
],
'Buenos Aires': [
'Santiago',
'Valentina',
'Lola',
],
'Saint Petersburg': [
'Sonja',
'Dunja',
'Iwan',
'Tanja',
],
};
userNamesGroupedByLocation[Symbol.iterator] = function() {
return {
next: () => {
return {
done: true,
value: 'hi',
};
},
};
}
// Calling the iterator function returns the iterator object
const iterator = userNamesGroupedByLocation[Symbol.iterator]();
console.log(iterator.next().value);
// "hi"
运行结果如下:
如果这样写的话,用 for...of 循环是遍历不出任何东西的,因为我们直接把 done 设成了 false(导致循环立即结束)。而且,用这种方式实现的话,我们也根本拿不到任何用户名。这恰恰就是我们一开始想要让这个对象变得可迭代的原因——为了能真正拿到数据。
三、实现迭代器
首先,我们需要获取对象的键,也就是那些代表城市的名字。我们可以通过对
this 关键字调用 Object.keys 来实现,这里的 this 指向该函数的父级对象,在本例中就是 userNamesGroupedByLocation 对象。只有当我们使用
function 关键字来定义这个可迭代函数时,才能通过 this 访问到这些键。如果我们用了箭头函数,这招就行不通了,因为箭头函数会继承其父级的作用域,导致 this 指向不对。const cityKeys = Object.keys(this);
我们还需要两个变量来追踪当前的迭代状态。let cityIndex = 0;
let userIndex = 0;
我们把这些变量定义在 iterator 函数里,但处于 next 函数之外。这样做能让我们在多次迭代之间保留这些数据。 在
next 函数里,我们首先需要利用之前定义的索引,取出当前城市对应的用户数组以及当前具体的用户。我们可以利用这些数据来调整现在的返回值了。
return {
next: () => {
const users = this[cityKeys[cityIndex]];
const user = users[userIndex];
return {
done: false,
value: user,
};
},
};

浙公网安备 33010602011771号