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-js 和 regenerator,不過這一整包,其實有點龐大,要 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:
- rollup-plugin-node-resolve 用來尋找
node_modules
目錄裡的模組 - rollup-plugin-commonjs 用來解析 CommonJS 型式的模組
然後用以下的設定:
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 這個專案。