边学边译JS工作机制-23.迭代器以及如何获得比generator更高的控制权

108 阅读6分钟

本系列其他译文请看[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是几乎不可能的,你需要向你的用户获取更多的上下文信息。