# 迭代器

在阐述这篇文章之前,首先看一道比较经典的面试题,可能看过之后大家将会为什么技术积累有多么的重要了。(在阅读本文之前,请确保你已经完全掌握Symbol的相关知识点。)

为了使得下面的代码按预期运行,请问在右侧的对象上需要进行什么操作?

const [a, b] = { a: 1, b: 2 };

既然这题放在Iterator这一节,那么肯定是跟Iterator的知识点相关了,如果你一时半会儿还没有任何思路的话,请先接着往下面看,然后你心中自然就会有答案了。

# 1、基本概念

遍历器(Iterator),它是一种接口,为各种不同的数据结构提供统一的访问机制。任何数据结构只要部署Iterator接口,就可以完成遍历操作(即依次处理该数据结构的所有成员)。

ES6之前只有数组对象这两种表示集合的数据结构,但是ES6增加了很多的数据结构(如MapSet),因此Iterator的出现可以使得这些数据结构的遍历有一套标准的方法,ES6创造了一种新的遍历命令for...of循环,Iterator接口主要供for...of消费。

遍历器接口(Iterable)、指针对象(Iterator)和next方法返回值的规格可以描述如下。

interface Iterable {
  [Symbol.iterator](): Iterator;
}

interface Iterator {
  next(value?: any): IterationResult;
}

interface IterationResult {
  value: any;
  done: boolean;
}

原生具备Iterator接口的数据结构如下。

  • Array
  • Map
  • Set
  • String
  • TypedArray
  • 函数的arguments对象
  • NodeList对象

也就是说,这些数据结构天生支持for-of遍历,并且我们可以拿上述这些数据结构自带的Iterator进行迭代:

// 手动调用String的Iterator
const str = "hello world";
// 得到string的Iterator
const it = str[Symbol.iterator]();
it.next();
// {value: 'h', done: false}
it.next();
// {value: 'e', done: false}
it.next();
// {value: 'l', done: false}
it.next();
// {value: 'l', done: false}
it.next();
// {value: 'o', done: false}
it.next();
// {value: ' ', done: false}
it.next();
// {value: 'w', done: false}
it.next();
// {value: 'o', done: false}
it.next();
// {value: 'r', done: false}
it.next();
// {value: 'l', done: false}
it.next();
// {value: 'd', done: false}
it.next();
// {value: undefined, done: true}
const str = "hello world";
for (const char of str) {
  console.log(char);
}

可以看到,如果我们自己去调用StringIterator的话,最后会输出一个{value: undefined, done: true}的结果,可是为什么for-of循环没有输出这个结果呢?

WARNING

这儿有一个重要的结论:for-of循环只会遍历迭代器done的值为false的结果

所以,如果用for-of循环遍历Generator生成的迭代器的话,无法处理到最后Generator函数的return值的(若有)。

另外,还有个易错点,就是有些同学初学时可能会犯这样的错误,为什么我已经明明按照规格定义了,但是采用for-of循环的时候却说这个结果不是可迭代的对象呢?

/**
 * 创建一个迭代器
 * @param {number[]} values
 * @returns
 */
function createIterator(values) {
  let idx = 0;
  return {
    next() {
      return {
        value: values[idx++],
        done: idx > values.length,
      };
    },
  };
}

for (const num of createIterator([1, 2, 3, 4, 5, 6])) {
  console.log(num);
}

代码运行时却得到了这样的一个错误:

createIterator is not a function or its return value is not iterable

其实你这个情况就是典型的自我感动,因为你知道它是一个迭代器,但是系统并不知道它是一个迭代器。

所以仅仅需要为其添加简单的一行代码即可:

/**
 * 创建一个迭代器
 * @param {number[]} values
 * @returns
 */
function createIterator(values) {
  let idx = 0;
  return {
    // 需要明确的告知系统,迭代时,就调用我自己就好
    [Symbol.iterator]() {
      return this;
    },
    next() {
      return {
        value: values[idx++],
        done: idx > values.length,
      };
    },
  };
}

for (const num of createIterator([1, 2, 3, 4, 5, 6])) {
  console.log(num);
}

# 2、调用 Iterator 的场合

如果你认真的阅读完本节,就可以回答本文开头提到的面试题了。除了for-of循环,主要在这几类场合会默认调用Iterator

  • 解构赋值: 对数组和Set结构进行解构赋值时,会默认调用Symbol.iterator方法。
  • 扩展运算符: 扩展运算符...也会调用默认的 Iterator 接口。
  • yield*: yield*后面跟的是一个可遍历的结构,它会调用该结构的遍历器接口。
let generator = function* () {
  yield 1;
  yield* [2, 3, 4];
  yield 5;
};

var iterator = generator();

iterator.next(); // { value: 1, done: false }
iterator.next(); // { value: 2, done: false }
iterator.next(); // { value: 3, done: false }
iterator.next(); // { value: 4, done: false }
iterator.next(); // { value: 5, done: false }
iterator.next(); // { value: undefined, done: true }
  • 其它场合:
  • for...of
  • Array.from()
  • Map(), Set(), WeakMap(), WeakSet()(比如 new Map([['a',1],['b',2]]))
  • Promise.all()
  • Promise.race()

因此,对于我们在文章开头提到的那题,我们只能从解构赋值的场景去思考,把右侧的内容当成一个数组,但是因为对象并不原生具备Iterator,因此,我们就可以给他补一个Iterator就可以使得代码正常运行了。

const [a, b] = {
  a: 1,
  b: 2,
  [Symbol.iterator]() {
    const keys = Object.keys(this);
    let idx = 0;
    const _this = this;
    return {
      next() {
        const i = idx++;
        return {
          value: _this[keys[i]],
          done: i >= keys.length,
        };
      },
    };
  },
};

需要注意的是,因为这种解构方式并不是原生支持的方式,有可能键取值顺序的关系,ab变量并不是一定拿到的就是 12,仅仅是这行代码能够正常工作而已(对象(Object)之所以没有默认部署Iterator接口,是因为对象的哪个属性先遍历,哪个属性后遍历是不确定的,需要开发者手动指定)。

# 3、遍历器对象的 return 方法和 throw 方法

遍历器对象除了具有next方法,还可以具有return方法和throw方法。如果你自己写遍历器对象生成函数,那么next方法是必须的,return方法和 throw方法是可选的。

return是一个无参数或者接受一个参数的函数,并返回符合IteratorResult接口的对象,其参数value通常等价于传递的value,并且done等于true。调用这个方法表明迭代器的调用者不打算调用更多的next方法,并且可以进行清理工作。

throw方法无参数或者接受一个参数的函数,并返回符合IteratorResult接口的对象,通常done等于true。调用这个方法表明迭代器的调用者监测到错误的状况,并且其参数exception通常是一个Error实例。

TS定义可以参考如下代码:

interface Iterator<T, TReturn = any, TNext = undefined> {
  // NOTE: 'next' is defined using a tuple to ensure we report the correct assignability errors in all places.
  next(...args: [] | [TNext]): IteratorResult<T, TReturn>;
  return?(value?: TReturn): IteratorResult<T, TReturn>;
  throw?(e?: any): IteratorResult<T, TReturn>;
}

# 异步迭代器

# 1、基本概念

上述代码都要求我们的代码是同步代码,即调用next函数时其必须立即返回结果。但实际上有些场景下肯定是存在异步情况的(设计模式有个模式叫做迭代器模式,而我们在实际的开发中需要将一些异步操作统一规格进行操作)。

ES2018引入了一个异步迭代器的概念,即同步迭代器返回结果是{ value: any; done: boolean },异步迭代器的返回结果是Promise<{value: any; done: boolean}>

/**
 * 创建一个异步迭代器
 * @param {number[]} values
 * @returns
 */
function createAsyncIterator(values) {
  let idx = 0;
  return {
    [Symbol.asyncIterator]() {
      return this;
    },
    next() {
      return new Promise((resolve) => {
        resolve({
          value: values[idx++],
          done: idx > values.length,
        });
      });
    },
  };
}

上述这个代码得到的迭代器,如果自己实现数据结构遍历其实还是不太容易的。因为程序的执行流程是异步的,循环遍历只能处理同步代码,是达不到我们想要的效果的。

如果有看到过async函数的spawn函数实现过程的小伙伴肯定脑袋中一下子就会有解决方案:递归

以下是我自己写的一个异步迭代器的遍历器:

/**
 * 异步迭代器遍历器
 * @param {AsyncIterator} it 异步迭代器
 */
function run(it) {
  if (!it || typeof it[Symbol.asyncIterator] !== "function") {
    console.log("the parameter must be an asynchronous iterator");
    return;
  }
  it.next().then(({ value, done }) => {
    if (!done) {
      run(it);
    }
    console.log(value);
  });
}

于是ES6又提供了一个新语法: for await...of,除了多了一个await关键字以外,它消费的接口是Symbol.asyncIterator,其他技术特征跟for-of没有什么区别。

for await...of 同样可以用于同步迭代器(其实也很好解释,采用Promise.resolve()将一个不是Promise的值包裹成一个Promise,规格就保持一致了嘛)

因为加上了await关键字,所有函数最外层就要套上async关键字(本文不考虑顶层await关键字语法),使用for await-of遍历这个异步迭代器,可以将代码书写成如下:

const it = createAsyncIterator([1, 2, 3, 4, 5, 6]);
(async function run() {
  // 多了一个await关键字
  for await (const num of it) {
    console.log(num);
  }
})();

代码真的是简洁了不少。

for await...of会等待前一个异步任务完成之后才会继续向后迭代,这个,我们可以在后面用一个异步Generator函数的例子得到证明。

但是:

WARNING

异步迭代器的next方法是可以一直调用的,并不需要等待先前的异步操作完成

其实这个语法特点也比较好理解,我们先调next函数相当于是在部署异步链,但是异步任务什么时候执行到这个位置,这就不得而知了。

用一个简单的例子来理解这个事儿,就比如湖南浏阳的烟花爆竹全国出名,他们在制作花炮时,首先将其一个一个的爆竹绑在一条预设的引线上,而这个引线什么时候点,就取决于最终花炮的所有者,当引线烧到了爆竹的位置,爆竹就爆炸了。

这个例子中,将爆竹绑在引线上,其实就好比我们在调用next函数,然后爆竹爆炸就是好比我们写在其Promisethen方法的回调执行了,而引线什么时候点燃,就取决于这个异步链的上游的Promise的状态改变。

而你用for await...of遍历,就会像是async函数内部的运行原理一样,是递归的一步一步的向后迭代的,每一步都需要等待上一步的异步任务完成,才会到下一个异步任务。

# 2、异步Generator函数

Generator那节,我们讨论了Generator函数的执行结果是一个迭代器,而如果在一个Generator函数前加上关键字async,那么这个函数就变成了异步Generator函数

function* bar() {}
const b = bar();
console.log(b[Symbol.toStringTag]); // Generator
console.log(b[Symbol.iterator]); // ƒ [Symbol.iterator]() { [native code] }

async function* foo() {}
const a = foo();
console.log(a[Symbol.toStringTag]); // AsyncGenerator
console.log(a[Symbol.asyncIterator]); // ƒ [Symbol.asyncIterator]() { [native code] }
console.log(a[Symbol.iterator]); // undefined

其实也没有什么大不了的,函数体内部肯定会出现await关键字罢了,其它的技术特征跟普通的Generator也差不多,不过因为有await关键字的加入,所以await语句后面的逻辑需要等待异步任务执行完成之后才能执行了。

以下是摘自阮一峰老师网络书籍《ES6入门》的一个例子:

async function* readLines(path) {
  let file = await fileOpen(path);
  try {
    while (!file.EOF) {
      yield await file.readLine();
    }
  } finally {
    await file.close();
  }
}

另外,我们继续可以用这个代码来验证,异步Generator函数里面有一个永远pendingPromise,执行这个函数运行的结果,发现除了Promise构造函数里面的内容输出了,其余啥都没有。

async function* foreverPendingTest() {
  await new Promise(() => {
    console.log("我就是啥都不做");
  });
  yield 1;
  yield 2;
  yield 3;
}

(async function () {
  for await (const val of foreverPendingTest()) {
    console.log(val);
  }
  console.log("异步遍历完成");
})();

也就是如果这个迭代器的next函数的then方法里面部署代码,一个都不会执行。这个例子,就是回答之前我们在异步迭代器开始的时候所提到的。

通过babelfor await-of的编译结果来看,确实也可以证明其内部的运行原理和async是一样的。

"use strict";

function asyncGeneratorStep(gen, resolve, reject, _next, _throw, key, arg) {
  try {
    var info = gen[key](arg);
    var value = info.value;
  } catch (error) {
    reject(error);
    return;
  }
  if (info.done) {
    resolve(value);
  } else {
    Promise.resolve(value).then(_next, _throw);
  }
}

function _asyncToGenerator(fn) {
  return function () {
    var self = this,
      args = arguments;
    return new Promise(function (resolve, reject) {
      var gen = fn.apply(self, args);
      function _next(value) {
        asyncGeneratorStep(gen, resolve, reject, _next, _throw, "next", value);
      }
      function _throw(err) {
        asyncGeneratorStep(gen, resolve, reject, _next, _throw, "throw", err);
      }
      _next(undefined);
    });
  };
}

function _asyncIterator(iterable) {
  var method,
    async,
    sync,
    retry = 2;
  for (
    "undefined" != typeof Symbol &&
    ((async = Symbol.asyncIterator), (sync = Symbol.iterator));
    retry--;

  ) {
    if (async && null != (method = iterable[async]))
      return method.call(iterable);
    if (sync && null != (method = iterable[sync]))
      return new AsyncFromSyncIterator(method.call(iterable));
    (async = "@@asyncIterator"), (sync = "@@iterator");
  }
  throw new TypeError("Object is not async iterable");
}

function AsyncFromSyncIterator(s) {
  function AsyncFromSyncIteratorContinuation(r) {
    if (Object(r) !== r)
      return Promise.reject(new TypeError(r + " is not an object."));
    var done = r.done;
    return Promise.resolve(r.value).then(function (value) {
      return { value: value, done: done };
    });
  }
  return (
    (AsyncFromSyncIterator = function AsyncFromSyncIterator(s) {
      (this.s = s), (this.n = s.next);
    }),
    (AsyncFromSyncIterator.prototype = {
      s: null,
      n: null,
      next: function next() {
        return AsyncFromSyncIteratorContinuation(
          this.n.apply(this.s, arguments)
        );
      },
      return: function _return(value) {
        var ret = this.s["return"];
        return void 0 === ret
          ? Promise.resolve({ value: value, done: !0 })
          : AsyncFromSyncIteratorContinuation(ret.apply(this.s, arguments));
      },
      throw: function _throw(value) {
        var thr = this.s["return"];
        return void 0 === thr
          ? Promise.reject(value)
          : AsyncFromSyncIteratorContinuation(thr.apply(this.s, arguments));
      },
    }),
    new AsyncFromSyncIterator(s)
  );
}

(function () {
  var _run = _asyncToGenerator(
    /*#__PURE__*/ _regeneratorRuntime().mark(function _callee() {
      var _iteratorAbruptCompletion,
        _didIteratorError,
        _iteratorError,
        _iterator,
        _step,
        num;

      return _regeneratorRuntime().wrap(
        function _callee$(_context) {
          while (1) {
            switch ((_context.prev = _context.next)) {
              case 0:
                _iteratorAbruptCompletion = false;
                _didIteratorError = false;
                _context.prev = 2;
                _iterator = _asyncIterator([1, 2, 3]);

              case 4:
                _context.next = 6;
                return _iterator.next();

              case 6:
                if (
                  !(_iteratorAbruptCompletion = !(_step = _context.sent).done)
                ) {
                  _context.next = 12;
                  break;
                }

                num = _step.value;
                console.log(num);

              case 9:
                _iteratorAbruptCompletion = false;
                _context.next = 4;
                break;

              case 12:
                _context.next = 18;
                break;

              case 14:
                _context.prev = 14;
                _context.t0 = _context["catch"](2);
                _didIteratorError = true;
                _iteratorError = _context.t0;

              case 18:
                _context.prev = 18;
                _context.prev = 19;

                if (
                  !(_iteratorAbruptCompletion && _iterator["return"] != null)
                ) {
                  _context.next = 23;
                  break;
                }

                _context.next = 23;
                return _iterator["return"]();

              case 23:
                _context.prev = 23;

                if (!_didIteratorError) {
                  _context.next = 26;
                  break;
                }

                throw _iteratorError;

              case 26:
                return _context.finish(23);

              case 27:
                return _context.finish(18);

              case 28:
              case "end":
                return _context.stop();
            }
          }
        },
        _callee,
        null,
        [
          [2, 14, 18, 28],
          [19, , 23, 27],
        ]
      );
    })
  );

  function run() {
    return _run.apply(this, arguments);
  }

  return run;
})()();

上述代码没有贴generator-runtime库的代码。另外,while循环中嵌套的switch-case可以不用看,因为这个是generator函数流转流程的代码,我们只需要把它看做一个状态转化的黑盒就行。

如果上述代码你阅读有困难,可以先查阅我关于Generator (opens new window)Async (opens new window)函数的文章。

重点在_asyncIterator函数上,如果一个迭代器不是异步的迭代器,它会尝试将其转化成异步迭代器,那就可以得出一个结论:

for await-of既能遍历同步迭代器也能遍历异步迭代器,而for-of只能遍历同步跌代器