本系列其他译文请看[JS工作机制 - 小白1991的专栏 - 掘金 (juejin.cn)] (juejin.cn/column/6988…
本文阅读指数:5
JS提供的迭代器,已经够开发者日常使用了。但是有的场景下,自定义一个迭代器会带来极大的便利性和性能上的优化。
概述
在任何语言中,遍历集合中的每一项都是很常见的操作。JS也不例外的提供了一系列方式来进行集合的遍历,从简单的for
循环到复杂的map()
和 filter()
.
迭代器(Iterators) 和 生成器(Generators) 带来了迭代的概念,并被内置的JS核心代码中,并且为用户提供了for…of
循环让用户自定义循环中的逻辑
迭代器
在JS中,迭代器是一个对象,定义了一个数列,以及在迭代结束时的返回值。
迭代器是实现了Iterator接口的任意对象。这个对象需要有一个next()
方法和一个返回值,这个返回值要包含两个属性:
value
: 数列中的下一个值done
: 数列全部被访问之后,这个值为true。如果此时value属性也有值,那就是迭代器的返回值。
迭代器创建之后,只能通过next()
方法迭代。访问到最后一个值时,再调用next()
将会返回{done: true}
使用迭代器
有时为了分配一个数组并遍历它,需要很多的资源。所以迭代器应该在必要的时候才使用。迭代器可以表示不限尺寸的数组。
看一个简单的使用迭代器创建斐波那契数列的例子
function makeFibonacciSequenceIterator(endIndex = Infinity) {
let currentIndex = 0;
let previousNumber = 0;
let currentNumber = 1;
return {
next: () => {
if (currentIndex >= endIndex) {
return { value: currentNumber, done: true };
}
let result = { value: currentNumber, done: false };
let nextNumber = currentNumber + previousNumber;
previousNumber = currentNumber;
currentNumber = nextNumber;
currentIndex++;
return result;
}
};
}
这个例子中,迭代器会生成斐波那契数字,一直到endIndex
。迭代器每次迭代,回返回当前的斐波那契数字。
我们看一下使用效果:
let fibonacciSequenceIterator = makeFibonacciSequenceIterator(5);
// Generates the first 5 numbers.
let result = fibonacciSequenceIterator.next();
while (!result.done) {
console.log(result.value); // 1 1 2 3 5 8
result = fibonacciSequenceIterator.next();
}
定义可迭代对象
上面的代码有一个潜在的问题,就是没有办法验证这个迭代器是否有效。虽然它的返回值中包含了next()
方法,但很有可能只是个巧合,很多对象都有这个方法,但是它们并不能迭代。
因此JS在定义可迭代对象时多了一些要求。
我们用for…of
来检验上面的例子,会发现JS是无法识别这个迭代对象的:
let fibonacciSequenceIterator = makeFibonacciSequenceIterator(5);
for (let x of fibonacciSequenceIterator) {
console.log(x);
}
代码会抛出异常
Uncaught TypeError: fibonacciSequenceIterator is not iterable
一些JS内置类型,比如Array
Map
,是默认可以迭代的,而其他的类型就不行。
普通对象要想可迭代,必须实现@@iterator
,同时具有一个Symbol.iterator
关键属性。这个属性是一个函数,并返回当前迭代到的内容。
上面的例子可以这么改造一下:
function makeFibonacciSequenceIterator(endIndex = Infinity) {
let currentIndex = 0;
let previousNumber = 0;
let currentNumber = 1;
let iterator = {};
iterator[Symbol.iterator] = () => {
return {
next: () => {
if (currentIndex >= endIndex) {
return { value: currentNumber, done: true };
}
const result = { value: currentNumber, done: false };
const nextNumber = currentNumber + previousNumber;
previousNumber = currentNumber;
currentNumber = nextNumber;
currentIndex++;
return result;
}
}
};
return iterator;
}
现在,我们就有了一个可迭代对象了,使用for…of
操作试一下:
let fibonacciSequenceIterator = makeFibonacciSequenceIterator(5);
for (let x of fibonacciSequenceIterator) {
console.log(x); //1 1 2 3 5 8
}
生成器
自定义迭代器用处很大,在某些场景下非常有效。但是创建和维护它们,需要非常小心的维护它们内部的状态。
而生成器函数提供了一个替代方案,允许开发者定义迭代的步骤,你可以写一个不会持续执行的函数,我们使用function*
语法来写。
调用生成器函数,不会初次执行代码,相反它返回了一个特殊类型的迭代器,称之为生成器。调用生成器的next方法生成一个值时,生成器函数就继续执行,直到遇到yield关键字。
生成器可以当做是一个函数,返回一系列的值而不是一个单独的值,并且它是需要被连续调用的。
生成器的语法包含一个yield
,它可以暂停方法的执行,直到请求下一个值。
看看如何使用生成器生成斐波那契数列
function* makeFibonacciSequenceGenerator(endIndex = Infinity) {
let previousNumber = 0;
let currentNumber = 1;
for (let currentIndex = 0; currentIndex < endIndex; currentIndex++) {
yield currentNumber;
let nextNumber = currentNumber + previousNumber;
previousNumber = currentNumber;
currentNumber = nextNumber;
}
}
let fibonacciSequenceGenerator = makeFibonacciSequenceGenerator(5);
for (let x of fibonacciSequenceGenerator) {
console.log(x);
}
可以看到这个实现更容易,也更好维护。
比生成器更高的控制权
迭代器显式定义了next()
函数,这是JS接口的需要。使用生成器,则会隐式添加next()
函数。这是迭代器生成有效可迭代对象的方式。
迭代器隐式定义的 next()
函数接受一个参数,这个参数可以用来修改迭代器的内部状态,传递给next()
的值会被 yield
声明接收到。
深度改造一下上面的案例,这样你可以控制每一步可以跳过多少数字
function* makeFibonacciSequenceGenerator(endIndex = Infinity) {
let previousNumber = 0;
let currentNumber = 1;
let skipCount = 0;
for (let currentIndex = 0; currentIndex < endIndex; currentIndex++) {
if (skipCount === 0) {
skipCount = yield currentNumber; // skipCount is the parameter passed through the invocation of `fibonacciSequenceGenerator.next(value)` below.
skipCount = skipCount === undefined ? 0 : skipCount; // makes sure that there is an input
} else if (skipCount > 0){
skipCount--;
}
let nextNumber = currentNumber + previousNumber;
previousNumber = currentNumber;
currentNumber = nextNumber;
}
}
let fibonacciSequenceGenerator = makeFibonacciSequenceGenerator(50);
console.log(fibonacciSequenceGenerator.next().value); // prints 1
console.log(fibonacciSequenceGenerator.next(3).value); // prints 5 since 1, 2, and 3 are skipped.
console.log(fibonacciSequenceGenerator.next().value); // prints 8
console.log(fibonacciSequenceGenerator.next(1).value); // prints 21 since 13 is skipped.
需要注意,第一次调用next()
传递的参数会被忽略
另一个重要的特性是,可以调用throw()
方法来让生成器抛出一个异常。迭代器当前挂起的上下文会抛出这个异常,
如果迭代器内部没有捕捉这个异常,它会通过调用throw()
向上传播,那么后续的next()
调用会将done属性设置为true。比如:
function* makeFibonacciSequenceGenerator(endIndex = Infinity) {
let previousNumber = 0;
let currentNumber = 1;
let skipCount = 0;
try {
for (let currentIndex = 0; currentIndex < endIndex; currentIndex++) {
if (skipCount === 0) {
skipCount = yield currentNumber;
skipCount = skipCount === undefined ? 0 : skipCount;
} else if (skipCount > 0){
skipCount--;
}
let nextNumber = currentNumber + previousNumber;
previousNumber = currentNumber;
currentNumber = nextNumber;
}
} catch(err) {
console.log(err.message); // will print ‘External throw’ on the fourth iteration.
}
}
let fibonacciSequenceGenerator = makeFibonacciSequenceGenerator(50);
console.log(fibonacciSequenceGenerator.next(1).value);
console.log(fibonacciSequenceGenerator.next(3).value);
console.log(fibonacciSequenceGenerator.next().value);
fibonacciSequenceGenerator.throw(new Error('External throw'));
console.log(fibonacciSequenceGenerator.next(1).value); // undefined will be printed since the generator is done.
生成器也是通过调用return(value)
方法来结束的。
let fibonacciSequenceGenerator = makeFibonacciSequenceGenerator(50);
console.log(fibonacciSequenceGenerator.next().value); // 1
console.log(fibonacciSequenceGenerator.next(3).value); // 5
console.log(fibonacciSequenceGenerator.next().value); // 8
console.log(fibonacciSequenceGenerator.return(374).value); // 374
console.log(fibonacciSequenceGenerator.next(1).value); // undefined
异步生成器
可以在异步上下文中定义和使用生成器。异步生成器可以异步的生成一系列的值。
异步的语法是很好理解的。在定义function*
时在加上async
就可以了。
那么在迭代生成的数列时,就需要使用await
关键字。
我们把上面的例子再改造一下:
async function* makeFibonacciSequenceGenerator(endIndex = Infinity) {
let previousNumber = 0;
let currentNumber = 1;
for (let currentIndex = 0; currentIndex < endIndex; currentIndex++) {
await new Promise(resolve => setTimeout(resolve, 1000)); // a simple timeout as an example.
yield currentNumber;
let nextNumber = currentNumber + previousNumber;
previousNumber = currentNumber;
currentNumber = nextNumber;
}
}
(async () => {
const fibonacciSequenceGenerator = makeFibonacciSequenceGenerator(6);
for await (let x of fibonacciSequenceGenerator) {
console.log(x); // 1, then 1, then 2, then 3, then 5, then 8 (with delay in between).
}
})();
异步生成器中,我们可以使用await,它是基于promise的。next()
方法会返回一个Promsie
。
有时候你不想使用生成器,但是你还是想定义一个迭代对象,你可以使用Symbol.asyncIterator
而不是 Symbol.iterator
。
虽然,相比迭代器,生成器更加简单一点,但是调试起来也麻烦一点。如果使用的是异步上下文的,就更麻烦了,当调用throw()
方法时,栈跟踪的信息非常有限,从这些信息中debug是几乎不可能的,你需要向你的用户获取更多的上下文信息。