Babel 編譯出來還是 ES 6?難道只能上 polyfill?

我知道 Babel 只編譯語法不編譯 API,可是這語法編譯出來卻是 API,我要不是看了代碼根本就發現不了,難道永遠都得引用 polyfill?不然我怎麼知道哪些語法會編譯出 ES 6 的 API?


您提到的「Babel 只編譯語法不編譯 API」說法並不完全正確,Babel 是處於構建時(也就是傳統Java等語言的編譯時),轉譯出來的結果在默認情況下並不包括 ES6 對運行時的擴展,例如,builtins(內建,包括 Promise、Set、Map 等)、內建類型上的原型擴展(如 ES6 對 Array、Object、String 等內建類型原型上的擴展)以及Regenerator(用於generators / yield)等都不包括在內。

那麼大伙兒都在這個問題里提到的 polyfill 和我說的運行時擴展分別是什麼呢?

core-js 標準庫

這是所有 Babel polyfill 方案都需要依賴的開源庫zloirock/core-js,它提供了 ES5、ES6 的 polyfills,包括 promises 、symbols、collections、iterators、typed arrays、ECMAScript 7+ proposals、setImmediate 等等。

如果使用了 babel-runtime、babel-plugin-transform-runtime 或者 babel-polyfill,你就可以間接的引入了 core-js 標準庫。題主所提到的 Array.from 就是來自於 core-js/array/from.js 。

regenerator 運行時庫

這是 Facebook 提供的 facebook/regenerator 庫,用來實現 ES6/ES7 中 generators、yield、async 及 await 等相關的 polyfills。在下面即將提到的 babel-runtime 中被引用。有些初學者遇到的「regeneratorRuntime is not defined」就是因為只在 preset 中配置了 stage-0 卻忘記加上 babel-polyfill。

如果使用了 babel-runtime、babel-plugin-transform-runtime 或者 babel-polyfill,你就可以間接的引入了 regenerator-runtime 運行時庫(非必選)。

babel-runtime 庫

babel-runtime 是由 Babel 提供的 polyfill 庫,它本身就是由 core-js 與 regenerator-runtime 庫組成,除了做簡單的合併與映射外,並沒有做任何額外的加工。

所以在使用時,你需要自己去 require,舉一個例子,如果你想使用 Promise,你必須在每一處需要用到 Promise 的 module 里,手工引入 promise 模塊:

const Promise = require("babel-runtime/core-js/promise");

由於這種方式十分繁瑣,事實上嚴謹的使用還要配合 interopRequireDefault() 方法使用,所以 Babel 提供了一個插件,即 babel-plugin-transform-runtime。

babel-plugin-transform-runtime 插件

這個插件讓 Babel 發現代碼中使用到 Symbol、Promise、Map 等新類型時,自動且按需進行 polyfill,因為是「自動」所以非常受大家的歡迎。

在官網中,Babel 提醒大家如果正在開發一個 library 的話,建議使用這種方案,因為沒有全局變數和 prototype 污染。

全局變數污染,是指 babel-plugin-transform-runtime 插件會幫你實現一個沙盒(sandbox),雖然你的 ES6 源代碼顯式的使用了看似全局的 Promise、Symbol,但是在沙盒模式下,Babel 會將它們轉譯成:

ES6 代碼

const sym = Symbol();

const promise = new Promise();

console.log(arr[Symbol.iterator]());

轉譯後的代碼

"use strict";

var _getIterator2 = require("babel-runtime/core-js/get-iterator");

var _getIterator3 = _interopRequireDefault(_getIterator2);

var _promise = require("babel-runtime/core-js/promise");

var _promise2 = _interopRequireDefault(_promise);

var _symbol = require("babel-runtime/core-js/symbol");

var _symbol2 = _interopRequireDefault(_symbol);

function _interopRequireDefault(obj) { return obj obj.__esModule ? obj : { default: obj }; }

var sym = (0, _symbol2.default)();

var promise = new _promise2.default();

console.log((0, _getIterator3.default)(arr));

你會發現,這個插件至始至終沒有在 Global 對象下掛在全局的 Symbol 和 Promise 變數。這樣一來,如果你引入的其他類庫使用了 bluebird 之類的第三方 polyfill 也不會受此影響。

那麼什麼是 prototype 污染呢,這就要說到 ES6 的 Array、String 等內建類型擴展了很多新方法,如 Array 原型上的 includes()、filter() 等新方法,babel-plugin-transform-runtime 插件是不會進行擴展修改的,很多人往往忽略了這一點。要區分的是,Array.from 等靜態方法(也有人稱類方法)還是會被插件 polyfill 的。

因此,babel-plugin-transform-runtime 這個插件更適合於開發類庫(library)時去使用,而不適合直接用在獨立的前端工程中。另外,它可以按需polyfill,所以從一定程度上控制了polyfill 文件的大小。

babel-polyfill

最後來到 babel-polyfill,它的初衷是模擬(emulate)一整套 ES2015+ 運行時環境,所以它的確會以全局變數的形式 polyfill Map、Set、Promise 之類的類型,也的確會以類似 Array.prototype.includes() 的方式去注入污染原型,這也是官網中提到最適合應用級開發的 polyfill,再次提醒如果你在開發 library 的話,不推薦使用(或者說絕對不要使用)。

不同於插件,你所要做的事情很簡單,就是將 babel-polyfill 一次性的引入到你的工程中,通常是和其他的第三方類庫(如 jQuery、React 等)一同打包在 vendor.js 中即可。在你寫程序的時候,你完全不會感知 babel-polyfill 的存在,如果你的瀏覽器已經支持 Promise,它會優先使用 native 的 Promise,如果沒有的話,則會採用 polyfill 的版本(這個行為與 babel-plugin-transform-runtime 一致),在使用 babel-polyfill 後,你不需要引入 babel-plugin-transform-runtime 插件和其他依賴的類庫。它的缺點也顯而易見,那就是占文件空間並且無法按需定製。

題外話,關於 require()

@劉易川 在評論區提到

對於轉譯之後的require語句要怎麼處理? (不能直接在瀏覽器中使用)

這是一個很好的問題,Babel ES2015 默認採用的是 CommonJS 模塊化(通過 transform-es2015-modules-commonjs 實現),所以會輸出 require(),這就是為什麼很多 Node.js 項目也採用 Babel 的原因,Webpack 會將 require() 轉譯成相應的瀏覽器級代碼,這一部分已經不是 polyfill 的範疇,require() 也不是 ES6 模塊化的規範。

如果想了解 Webpack 2.0/3.0 時代下的 JS 模塊化,可以參考我的另兩個回答:Henry Li:當下如何擁抱ES6的模塊化機制?,Henry Li:ECMAScript 6 的模塊相比 CommonJS 的require (...)有什麼優點?


很簡單,因為這樣編譯才能保證正確的語義。

當然如果你對語義正確沒有那麼大的追求,可以用buble之類的東西,但是我建議你不要用,因為buble的坑和需要你了解的細節更多。我個人非常反對buble的設計理念。

至於這樣編譯導致的問題,如需要用polyfill且比較難確定要載入哪些polyfill從而很難控制polyfill的大小——確實不好辦,這是babel目前的問題,且短期看不到很好的官方解決可能。不過官方之所以不解決是因為這些問題不是那麼重要(比如node平台下完全無所謂),解決起來又很難。所以先忍著就好。長期來說早晚會有解決方案的。


Babel 包含編譯和polyfill兩部分


語法糖類的應該可以轉,簡單的新特性和api應該也可以轉,一些複雜核心的新特性和api應該只能polyfill了吧


編譯的是語法 沒有的方法或者屬性需要polyfill


確實是這樣的,比如說你使用了for...of做循環,那麼它最後編譯完就是調用這個對象的Symbol.iterator()方法,如果不載入polyfill的話,那麼在瀏覽器原生不支持es6新對象的環境下,是會有問題的。其實babel還有個plugin叫transform-runtime,他會把他預先指定的一些原生對象或者原生方法編譯成非非原生的方法,比如你用了Promise。可能編譯出來就是PromiseClass這樣子了。不影響原生,但是也不使用原生,用的是core-js裡面的實現,性能上會有一些問題。


Babel 默認只轉換新的 JavaScript 句法(syntax),而不轉換新的 API,比如Iterator、Generator、Set、Maps、Proxy、Reflect、Symbol、Promise等全局對象,以及一些定義在全局對象上的方法(比如Object.assign)都不會轉碼。

舉例來說,ES6 在Array對象上新增了Array.from方法。Babel 就不會轉碼這個方法。如果想讓這個方法運行,必須使用babel-polyfill,為當前環境提供一個墊片。

安裝命令如下。

$ npm install --save babel-polyfill

然後,在腳本頭部,加入如下一行代碼。

import "babel-polyfill";

// 或者

require("babel-polyfill");

Babel 默認不轉碼的 API 非常多,詳細清單可以查看babel-plugin-transform-runtime模塊的definitions.js文件。

ECMAScript 6入門


推薦閱讀:

Babel的ES6翻譯存在明顯的缺陷,為什麼沒怎麼見人提過這種大坑?
有了Babel的話還在使用TypeScript的優勢在哪?
如何評價 Webpack 2 新引入的 Tree-shaking 代碼優化技術?

TAG:前端開發 | JavaScript | ECMAScript2015 | Babel |