Babel 编译出来还是 ES 6?难道只能上 polyfill?
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,包括 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 (...)有什么优点?
我专门研究过这块代码,写了个系列,可以看下
https://zhuanlan.zhihu.com/p/58624930
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
,Promise
和 includes
都没有转换 ,这是为什么
Babel
把 Javascript
语法 分为 syntax
和 api
先说 api
, api
指那些我们可以通过 函数重新覆盖的语法 ,类似 includes,map,includes,Promise
,凡是我们能想到重写的都可以归属到 api
啥子是 syntax
,像 箭头函数,let,const,class, 依赖注入 Decorators
,等等这些,我们在 Javascript
在运行是无法重写的,想象下,在不支持的浏览器里不管怎么样,你都用不了 let 这个关键字
千万要get到上面这2个点,非常重要,很多人以为只要 引用了 Babel
就不会出现兼容性问题了,这个是大错特错的
syntax
这个关键字 Babel
的官网只是一笔带过,直译又不准确,网上很多文章在说 ployfill
和 transform-runtime
的差别都没说到点上,还互相瞎鸡儿抄,这个点上小编还是很自信的,按照自己的理解,说出二者的差别(默默的给自己加个鸡腿)
那 Babel
只负责 转换 syntax
, includes,map,includes
这些 API
层面的 怎么办, Babel
把这个放在了 单独放在了 ployfill
这个模块处理
Babel
这个设计非常好, 把 Javascript
语法抽象成2个方面的, syntax
和 ployfill
独立开来,分而治理,6to5
一开始设计是把二者放在一起的,大家想想 ployfill
随着浏览器的不同,差异是非常大的,2个要是在一起 代码的耦合性就太大了,到处都是if else