20K for...of

for...of 是 ECMAScript 2016 的新語法,有了他之後,要用迴圈跑過陣列不用像以前一樣先用for...in或是用傳統的取長度,然後i++的方法:

var arr = [1, 2, 3];
var i, v, len;

for (i in arr) {
    v = arr[i];
    console.log(v);
}

for (i = 0, len = arr.length; i < len; i++) {
    v = arr[i];
    console.log(v);
}

現在只要用簡單的for...of就可以了:

var arr = [1, 2, 3];

for (let v of arr) {
    console.log(v);
}

不過目前還是需要考慮只有 ECMAScript 5 的環境,例如 IE11,所以一般都還是會用像是 Babel 之類的 transpiler 來把 ES2015 的 syntax 轉成 ES5 的 code,結果轉出來如下:

"use strict";

var arr = [1, 2, 3];

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 v = _step.value;

        console.log(v);
    }
} catch (err) {
    _didIteratorError = true;
    _iteratorError = err;
} finally {
    try {
        if (!_iteratorNormalCompletion && _iterator.return) {
            _iterator.return();
        }
    } finally {
        if (_didIteratorError) {
            throw _iteratorError;
        }
    }
}

結果其實有點意外,一個簡單的for...of竟然變的這麼長,事實上是因為for...of其實沒想像中簡單,因為它可以用的地方其實不只是陣列,而是 iterable 物件,不過為了要完整的支援for...of,就變成需要有 iterator, generator, symbol 等等的支援,當然上面的程式碼不能在 ES5 環境下執行,而 Babel 依靠的是 babel-polyfill,裡面其實就是 core-jsregenerator,不過這一整包,其實有點龐大,要 228KB,即使最小化之後也還要 95KB,所以,就想著是不是能夠只捆包進需要的部分就好了,研究過後,發現有 Babel plugin 叫做 transform-runtime,套用上去後:

import _getIterator from "babel-runtime/core-js/get-iterator";
var arr = [1, 2, 3];

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

try {
  for (var _iterator = _getIterator(arr), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true) {
    var v = _step.value;

    console.log(v);
  }
} catch (err) {
  _didIteratorError = true;
  _iteratorError = err;
} finally {
  try {
    if (!_iteratorNormalCompletion && _iterator.return) {
      _iterator.return();
    }
  } finally {
    if (_didIteratorError) {
      throw _iteratorError;
    }
  }
}

可以看到原來用Symbol取 iterator 的地方變成用_getIterator了,而且還有一行:

import _getIterator from "babel-runtime/core-js/get-iterator";

如果要真的把這部分也打包進來,則需要讓 bundler 處理,我個人是偏好 rollup,搭配以下兩個 plugin:

然後用以下的設定:

babel({
  exclude: 'node_modules/**',
  plugins: ['transform-runtime'],
  presets: ['es2015-loose-rollup'],
  runtimeHelpers: true
}),
nodeResolve({ jsnext: true }),
commonjs({
  include: 'node_modules/**'
})

結果,就可以得到夢寐以求的 20KB 的程式碼了,當然 20KB 的部分不是預期的啦,相較於一開始的程式碼只有 72Bytes,為了一個for...of變成 20KB 好像有點本末倒置,畢竟我只有要在 Array 上用,難道不能只是簡單的轉成for...in型式嗎。

事實上是有辦法的,第一個就是改寫 TypeScript,TypeScript 對於for...of只有兩種處理方法,而且結果都不會如此膨脹,第一種就是變成for...in,第二種則是不變動,保留for...of的語法,後者是在 target 設定成 ES6 的時候使用的,官網也有相關的說明

第二種方法則是用 Bublé 取代 Babel 做為 transpiler,Bublé 是 rollup 的作者 Rich Harris 的另外一個作品,我個人是蠻喜歡他的哲學的,Bublé 的哲學則是對於 code 做簡單、直接明瞭的轉換,所以for...of就只會轉成for...in的型式,不過也因此無法支援 iterable 物件,所以預設是不開啟支援的,歸類在 dangerious transofrm 之下,另外 Bublé 也還不支援 Async/Await,因為要做出支援 ES3/5 的同樣效果的 code 會增加太多的複雜度,不符合他的哲學理念,所以目前還沒有計畫支援,這點倒是 TypeScript 支援比較完整,目前的 2.1 RC 已經支援把 Async/Await 轉成 ES3/5 的版本了。

最後結論,基本上就是個取捨,Babel、TypeScript、Bublé 各自有它們的優缺點,所以只能看情況選擇了,如果要 Map/Set 也要在這些物件上用for...of語法然後也要 Async/Await,那就只能用 Babel 加上 babel-polyfill;如果可以不要 Map, Set 或是可以接受不在這些物件上使用for...of語法(還可以用 forEach),那可以選擇 TypeScript,然後加上 Map/Set 的 polyfill,如果不用 Async/Await,也不用 Map/Set 的話,可以考慮用個 Bublé 看看。不過如果完全不需要考慮 ES3/5 的環境的話(Edge, Firefox, Chrome 都已經對 ES2015 支援很完整了),好像問題突然就小很多了XD,最後附上這篇文章提到的各種作法產生的檔案參考,目前都放在 github 上的 20k-for-of 這個專案。


更之前的文章