Babel 编译出来还是 ES 6?难道只能上 polyfill?

[图片] 我知道 Babel 只编译语法不编译 API,可是这语法编译出来却是 API,我要不是看了代码根本就发现不了,难道永远都得引用 polyfi…
关注者
171
被浏览
72,824

11 个回答

您提到的“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,包括 promisessymbolscollections、iterators、typed arraysECMAScript 7+ proposalssetImmediate 等等。

如果使用了 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 (...)有什么优点?

我专门研究过这块代码,写了个系列,可以看下

zhuanlan.zhihu.com/p/58

polyfill 是看你的项目所需要的运行的浏览器环境决定的,不是必须的

Babel只是转换syntax层语法,所有需要@babel/ployfill来处理API兼容,又因为ployfill体积太大,所以通过preset的 useBuiltIns 来实现按需加载,再接着为了满足 npm 组件开发的需要 出现了@babel/runtime来做隔离


下面上一段 常见代码

转换前代码

let array = [1, 2, 3, 4, 5, 6];
array.includes(item => item > 2);
new Promise()

Babel转换后代码

var array = [1, 2, 3, 4, 5, 6];
array.includes(function (item) {
  return item > 2;
});
new Promise()

Babel 默认只是转换了 箭头函数 let ,Promiseincludes 都没有转换 ,这是为什么

BabelJavascript 语法 分为 syntaxapi

先说 api , api 指那些我们可以通过 函数重新覆盖的语法 ,类似 includes,map,includes,Promise,凡是我们能想到重写的都可以归属到 api

啥子是 syntax ,像 箭头函数,let,const,class, 依赖注入 Decorators,等等这些,我们在 Javascript 在运行是无法重写的,想象下,在不支持的浏览器里不管怎么样,你都用不了 let 这个关键字

千万要get到上面这2个点,非常重要,很多人以为只要 引用了 Babel 就不会出现兼容性问题了,这个是大错特错的

syntax 这个关键字 Babel 的官网只是一笔带过,直译又不准确,网上很多文章在说 ployfilltransform-runtime 的差别都没说到点上,还互相瞎鸡儿抄,这个点上小编还是很自信的,按照自己的理解,说出二者的差别(默默的给自己加个鸡腿)

Babel 只负责 转换 syntax , includes,map,includes 这些 API 层面的 怎么办, Babel 把这个放在了 单独放在了 ployfill 这个模块处理

Babel 这个设计非常好, 把 Javascript 语法抽象成2个方面的, syntaxployfill 独立开来,分而治理,6to5 一开始设计是把二者放在一起的,大家想想 ployfill 随着浏览器的不同,差异是非常大的,2个要是在一起 代码的耦合性就太大了,到处都是if else