Codelet Keep code simple stupid

JavaScript for-of循环

JavaScript中的循环语句大致可以分为:条件循环(for/while),属性枚举(for-in),Array方法 (forEach)几种,而ES6又新增了一种循环语句for-of,用于可迭代对象的循环。

迭代器实际上是一种设计模式,用于提供顺序访问容器内元素的方法,而不需要了解具体的实现。如果说之前 的JS中,迭代器只是以模式来运用的话,那么在ES6之后JS则是对迭代器提供了系统级的支持,任何JS对象 只要实现了可迭代协议,就可以通过统一的for-of方式实现迭代。

可迭代对象: 对象实现了@@iterator方法(即具有Symbol.iterator属性),则认为该对象是可迭代的,可以用 for-of语句遍历,这种方式被称为可迭代协议。(一般@@iterator方法会返回一个迭代器对象,如果 该方法返回的不是一个迭代器对象,则该对象不是良构的可迭代对象,可能会导致迭代失败)

迭代器: 迭代器遵循迭代器协议产生一系列的值,迭代器具有next方法,该方法会返回一个对象,其中done属性 表示迭代是否结束,value属性则是本次迭代的元素值。

所以实现自定义迭代器的关键在于实现next方法,通过其返回对象的done属性及value属性来控制 迭代的流程,然而有没有更加简单直接的办法呢?其实通过生成器就可以做到,那么下面就介绍一下什么是 生成器。

生成器: 通过调用生成器函数function*可以得到一个生成器对象,生成器对象的next结果是根据生成器函数的 执行过程确定的,函数通过yield语句产生迭代相关的元素值,函数退出时迭代也随之结束。生成器是遵循 迭代器协议的(具有next方法),也就是说生成器也是一种迭代器,除此之外生成器还具有returnthrow方法,用于终止迭代。其实生成器可以看做实现迭代协议的一种快捷方式,yield语句可以看做 是实现next方法的语法糖。

下面就看一下Babel是怎么处理for-of语句的

let arr = [1, 2, 3, 4]

for (let n of arr) {
  console.log(n)
}

我格式化了一下代码,并添加了注释,其中的关键就在于通过[Symbol.iterator]()获取迭代器对象, 通过iterator.next()获取元素值并判断是否继续迭代。

"use strict";

var arr = [1, 2, 3, 4];

var _iteratorNormalCompletion = true;
var _didIteratorError = false;
var _iteratorError = undefined;

try {
  for (var _iterator = arr[Symbol.iterator](),
      _step;
      !(_iteratorNormalCompletion = (_step = _iterator.next()).done);
      _iteratorNormalCompletion = true) { // 单次迭代成功结束
    var n = _step.value;
    // 使用元素
    console.log(n);
  }
} catch (err) {
  _didIteratorError = true; // 迭代异常退出
  _iteratorError = err;     // 保存异常
} finally {
  try {
    if (!_iteratorNormalCompletion && _iterator.return) {
      _iterator.return();   // 迭代异常,结束迭代器
    }
  } finally {
    if (_didIteratorError) {
      throw _iteratorError; // 重新抛出异常
    }
  }
}

上面的代码其实很易懂,较难理解的地方就在于iteratorNormalCompletion变量以及异常处理部分, 下面就来解释一下。

其中iteratorNormalCompletion变量用于标记迭代是否正常退出,如果在for-of循环中执行了 break语句或者抛出了异常,则会执行清理工作,停止迭代器。

其中的异常处理用了两层try语句,其中 第一层用于捕获循环中的异常,第二个try很特别,这是一个try-finally语句,它会抛出return 函数中抛出的异常,而且会保证finally语句总是执行,保证iteratorError会优先被抛出。

在理解了try-finally语句之后,didIteratorErroriteratorError两个变量的作用也就很 清楚了,采用re-throw方式来控制异常的优先顺序,假设throw语句直接写在catch之中,那么当 之后的return函数执行再发生异常时,后面的异常会覆盖迭代产生的异常,从而导致循环语句中的错误栈 信息丢失,不利于代码调试。

可以验证一下

let obj = {
  [Symbol.iterator] () {
    let count = 0;
    return {
      next () {
        if (count < 10) {
          return {
            done: false,
            value: count++
          };
        } else {
          return {
            done: true,
            value: undefined
          };
        }
      },
      return () {
        throw new Error('Invalid return');
        count = 0;
      }
    };
  }
};

// 如果只使用单层`try`,应该改写成如下代码
var _iteratorNormalCompletion = true;

try {
  for (var _iterator = obj[Symbol.iterator](),
      _step;
      !(_iteratorNormalCompletion = (_step = _iterator.next()).done);
      _iteratorNormalCompletion = true) {
    var n = _step.value;
    if (n === 5) {
      throw new Error('Bad state');
    }
    console.log(n);
  }
} catch (err) {
  throw err;
} finally {
  if (!_iteratorNormalCompletion && _iterator.return) {
    _iterator.return();
  }
}
// Uncaught Error: Invalid return

// 使用原生的`for-of`循环
for (let n of obj) {
  if (n === 5) {
    throw new Error('Bad state');
  }
  console.log(n);
}
// Uncaught Error: Bad state

最后再提一下生成器,注意生成器对象既是迭代器也是可迭代对象,所有内建迭代器也是可迭代对象,但是 自定义的迭代器对象可能不具有此性质,需要特别注意。

function* genNums () {
  let i = 0
  while (true) { yield ++i }
}

let gen = genNums()

gen[Symbol.iterator] !== undefined // true
gen[Symbol.iterator]() === gen // true
for (let n of gen) { ... }
// 可见生成器的迭代器是生成器本身,因此生成器对象是可迭代对象,可以用for-of遍历

let iter = [][Symbol.iterator]()

iter[Symbol.iterator] !== undefined // true
iter[Symbol.iterator]() === iter // true
for (let n of iter) { ... }
// 数组的迭代器对象也是可迭代对象,可以用for-of遍历

// 其根本原因是gen/iter二者都继承自`%IteratorPrototype%`原型,此原型的`@@iterator`方法
// 会返回this对象本身
gen[Symbol.iterator] === iter[Symbol.iterator] // true

参考资料:
[1] MDN - Iteration Protocols
[2] MDN - Generator