ES6 借鉴了其他编程语言的特性,为 JavaScript 带来了 for…of 循环语法,用于遍历数组等数据结构。当然,由于是 ES6 的特性,我们使用 for…of 的时候,依然要借助 Babel 进行转码。我们来看看 Babel 是如何处理 for…of 代码的。
ES6 原生代码如下。为避免干扰,这里不使用 ES6 其他特性。
var names = ['paul', 'jordan', 'griffin'];
for (var name of names) {
console.log(name);
}
Babel 转码后结果如下:
'use strict';
var names = ['paul', 'jordan', 'griffin'];
var _iteratorNormalCompletion = true;
var _didIteratorError = false;
var _iteratorError = undefined;
try {
for (var _iterator = names[Symbol.iterator](), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true) {
var name = _step.value;
console.log(name);
}
} catch (err) {
_didIteratorError = true;
_iteratorError = err;
} finally {
try {
if (!_iteratorNormalCompletion && _iterator.return) {
_iterator.return();
}
} finally {
if (_didIteratorError) {
throw _iteratorError;
}
}
}
我们观察到 Babel 转换后的代码里第 9 行仍然出现了 ES6 的特性——Symbol.iterator,这是为什么呢?我们先来探究一下 for…of 的实现原理。
for…of 在对数据结构进行循环时,背后实际上是调用了该数据结构的 Iterator 接口。一种数据结构只要具有 Iterator 接口,我们就可以认为该数据结构是“可遍历的”(iterable)。原生数据结构中具有“可遍历”属性的包括数组、Set、Map、以及字符串之类的类数组对象等。具体到 Iterator 接口上,ES6 规定,默认的 Iterator 接口部署在该数据结构的 Symbol.iterator 属性上(Symbol 是 ES6 新增的原始数据类型,表示独一无二的值,具体参见 ES6 文档),该属性本身是一个函数,执行该函数会返回一个指针对象。该指针对象称为遍历器,其必须包含一个 next 方法,不断调用 next 方法可以使指针从数据结构的第一个成员一直指向最后一个成员,即调用 next 方法会返回数据结构当前成员的信息,该信息为一个对象,包含 value 和 done 两个属性。value 是当前成员的值,done 是一个布尔值,表示遍历是否结束。以上的理论有点抽象,我们来模拟一个“可遍历”的数据结构:
const iterableData = {
data: ['paul','jordan','griffin','redick','rivers'],
dataIndex: 0,
//Symbol 类型的值作为对象属性时必须使用方括号结构
[Symbol.iterator]: function () {
var self = this;
return {
next: function () {
return {
value: self.data[self.dataIndex++],
done: self.dataIndex < self.data.length? false: true
};
}
};
}
};
for (let item of iterableData) {
console.log(item);
}
// paul, jordan, griffin, redick, rivers
可以看到,只要一个数据结构具有符合要求的 Symbol.iterator 属性,就可以通过 for…of 遍历(事实上,解构赋值、扩展运算符、yield* 等 ES6 特性也是调用该属性接口)。
现在,我们回过头来看 Babel 转换 for…of 循环的代码,其本质上还是通过调用 Iterator 接口(注意第 9 行),将 for…of 转换为传统的 for 循环,并在每次循环中调用遍历器的 next 方法来吐出数组中的值。如果在循环调用过程中出现错误,遍历器中如含有预定义的 return 函数(参见 ES6 文档中遍历器对象的规范 ),则调用之,否则直接抛出错误。
所以,问题就出现了,即使调用 Babel 对 for…of 循环进行转码,我们实际上还是无法完全摆脱 ES6 的特性——在不支持 Symbol 的环境下,代码仍然会报错。因为 Babel 默认只转换新的 JavaScript 句法(syntax),而不转换 Proxy、Set、Promise、Symbol 等新的 API。所以,在对兼容性要求较高时,确实要慎重使用 for…of 语法,即使我们有 Babel 这件神兵利器。
实际上,要想完全抹平 ES6 特性带来的新 API 也是可行的,只要在项目中引入 babel-polyfill 并配置好即可,但是这样带来的另一个问题就是因为 babel-polyfill 本身的体积,我们的代码也会变庞大不少。所以此举有利有弊,需要根据实际情况进行权衡。