TAT.heyli babel 到底将代码转换成什么鸟样?
In Web开发 on 2016年05月15日 by view: 3,802
4


原文链接

前言

将 babel 捧作前端一个划时代的工具一定也不为过,它的出现让许多程序员幸福地用上了 es6 新语法。但你就这么放心地让 babel 跑在外网?反正我是不放心,我就曾经过被坑过,于是萌生了研究 babel 代码转换的想法。本文不是分析 babel 源码,仅仅是看看 babel 转换的最终产物。

es6 在 babel 中又称为 es2015。由于 es2015 语法众多,本文仅挑选了较为常用的一些语法点,而且主要是分析 babel-preset-2015 这个插件(react 开发的时候,常在 webpack 中用到这个 preset)。

babel-preset-2015

打开 babel-preset2015 插件一看,一共 20 个插件。熟悉 es2015 语法的同志一看,多多少少能从字面意思知道某个插件是用于哪种语法的转换

  • babel-plugin-transform-es2015-template-literals => es2015 模板

  • babel-plugin-transform-es2015-literals

  • babel-plugin-transform-es2015-function-name => 函数 name 属性

  • babel-plugin-transform-es2015-arrow-functions => 箭头函数

  • babel-plugin-transform-es2015-block-scoped-functions => 函数块级作用域

  • babel-plugin-transform-es2015-classes => class 类

  • babel-plugin-transform-es2015-object-super => super 提供了调用 prototype 的方式

  • babel-plugin-transform-es2015-shorthand-properties => 对象属性的快捷定义,如 obj = { x, y }

  • babel-plugin-transform-es2015-computed-properties => 对象中括号属性,如 obj = {['x]: 1}

  • babel-plugin-transform-es2015-for-of => 对象 for of 遍历

  • babel-plugin-transform-es2015-sticky-regex

  • babel-plugin-transform-es2015-unicode-regex

  • babel-plugin-check-es2015-constants => const 常量

  • babel-plugin-transform-es2015-spread => 对象扩展运算符属性,如...foobar

  • babel-plugin-transform-es2015-parameters => 函数参数默认值及扩展运算符

  • babel-plugin-transform-es2015-destructuring => 赋值解构

  • babel-plugin-transform-es2015-block-scoping => let 和 const 块级作用域

  • babel-plugin-transform-es2015-typeof-symbol => symbol 特性

  • babel-plugin-transform-es2015-modules-commonjs => commonjs 模块加载

  • babel-plugin-transform-regenerator => generator 特性

var, const and let

const 和 let 现在一律转换成 var。那 const 到底如何保证不变呢?如果你在源码中第二次修改 const 常量的值,babel 编译会直接报错。
转换前

转换后:

那 let 的块级作用怎么体现呢?来看看下面例子,实质就是在块级作用改变一下变量名,使之与外层不同。
转换前:

转换后:

赋值解构

写 react 的时候,我们使用负值解构去取对象的值,用起来非常爽,像这样:

我们来看看转换的结果:

至于数组呢?如果是一个匿名数组,则 babel 会帮你先定义一个变量存放这个数组,然后再对需要赋值的变量进行赋值。
转换前:

转换后:

看到这个,感觉转换结果跟我们想的还蛮一致。哈哈,使用的噩梦还没开始。

如果使用匿名对象直接进行赋值解构会怎样呢?如下。babel 为了使接收的变量唯一,直接就将匿名对象里的属性拼在一起,组成接收这个匿名对象的变量,吓得我赶紧检查一下项目里有没有这种写法。
转换前:

转换后:

还有一种对象深层次的解构赋值:
转换前:

转换后:

babel 在代码顶部生产了一个公共的代码_slicedToArray。大概就是将对象里面的一些属性转换成数组,方便解构赋值的进行。但 Symbol.iterator 的兼容性并不好(如下图),还是谨慎使用为妙。
3

另外,下面这种对字符串进行赋值解构也同样使用到_slicedToArray 方法:

函数参数默认值及扩展运算符

在 es5 的年代,一般我们写参数的默认值都会这么写:

我们来看看 babel 的转换办法:

babel 这里使有了 arguments 来做判。第一种情况涉及解构赋值,因此 x 和 y 的值还是有可能是 undefined 的。至于第二种情况,则会保证 2 个参数的默认值分别是 1 和 2.

再来看一种。...y 代表它接收了剩下的参数。也就是 arguments 除了第一个标号的参数之外剩余的参数。
转换前:

转换后:

箭头函数

剪头函数其实主要是省了写函数的代码,同时能够直接用使外层的 this 而不用担心 context 切换的问题。以前我们一般都要在外层多写一个_this/self 直向 this。babel 的转换办法其实跟我们的处理无异。
转换前:

转换后:

对象的能力增强

对象属性的快捷定义

转换前:

转换后:

对象中括号属性

es2015 开始新增了在对象中用中括号解释属性的功能,这对变量、常量等当对象属性尤其有用。
转换前:

转换后:

看似简单的属性,babel 却大动干戈。新增了一个_defineProperty 函数,给新建的_obj = {} 进行属性定义。除此之外使用小括号包住一系列从左到右的运算使整个定义更简洁。

使用 super 去调用 prototype

以前我们一般都用 obj.prototype 或者尝试用 this 去往上寻找 prototype 上面的方法。而 babel 则自己写了一套在 prototype 链上寻找方法/属性的算法。
转换前

转换后:

Object.assign 和 Object.is

es6 新增的 Object.assign 极大方便了对象的克隆复制。但 babel 的 es2015 preset 并不支持,所以没对其进入转换,这会使得一些移动端机子遇到这种写法会报错。所以一般开发者都会使用 object-assign 这个 npm 的库做兼容。

Object.is 用于比较对象的值与类型,es2015 preset 同样不支持编译。

es6 模板

多行字符串

转换前:

转换后:

字符中变量运算

转换前:

转换后:

标签模板

es6 的这种新特性给模板处理赋予更强大的功能,一改以往对模板进行各种 replace 的处理办法,用一个统一的 handler 去处理。babel 的转换主要是添加了 2 个属性,因此看起来也并不算比较工程浩大的编译。
转换前:

转换后:

模块化与类

类 class

javascript 实现 oo 一直是非常热门的话题。从最原始时代需要手动维护在构造函数里调用父类构造函数, 到后来封装好函数进行 extend 继承,再到 babel 出现之后可以像其它面向对象的语言一样直接写 class。es2015 的类方案仍然算是过渡方案,它所支持的特性仍然没有涵盖类的所有特性。目前主要支持的有:

  • constructor

  • static 方法

  • get 方法

  • set 方法

  • 类继承

  • super 调用父类方法。

转换前:

转换后(由于代码太长,先省略辅助的方法):

先前在用 react 重构项目的时候,所有的 react 组件都已经摒弃了 es5 的写法,一律采用了 es6。用类的好处写继续更加方便,但无法用 mixin,需要借助更新的 es7 语法中的 decorator 才能够实现类 mixin 的功能 (例如 pureRender)。但这次分析完 babel 源码之后,才发现原来 babel 在实现 class 特性的时候,定义了许多方法,尽管看起来并不太优雅。

模块化

在开发 react 的时候,我们往往用 webpack 搭配 babel 的 es2015 和 react 两个 preset 进行构建。之前看了一篇文章对 babel 此处的模块加载有些启发(《分析 Babel 转换 ES6 module 的原理》)。

示例:

通过 webpack 与 babel 编译后:

es6 的模块加载是属于多对象多加载,而 commonjs 则属于单对象单加载。babel 需要做一些手脚才能将 es6 的模块写法写成 commonjs 的写法。主要是通过定义__esModule 这个属性来判断这个模块是否经过 babel 的编译。然后通过_interopRequireWildcard 对各个模块的引用进行相应的处理。

另一个发现是,通过 webpack 打包 babel 编译后的代码,每一个模块里面都包含了相同的类继承帮助方法,这是开发时忽略的。由此可看,在开发 react 的时候用 es5 的语法可能会比使用 es6 的 class 能使 js bundle 更小。

babel es2015 loose mode

开发家校群的时候,在 android4.0 下面报 esModule 错误的问题,如下:
Uncaught TypeError: Cannot assign to read only property '__esModule' of #<Object>。

经查证,发现是构建中 babel-es2015 loader 的模式问题,会导致 Android4.0 的用户有报错。只需要使用 loose mode 就可以解决问题。下面是相关的 stackoverflow issue 以及对应解决问题的 npm 包。

那么 es2015 和 normal mode 和 loose mode 有什么区别呢,这个出名的博客略有介绍:Babel 6: loose mode

实质就是(作者总结)normal mode 的转换更贴近 es6 的写法,许多的 property 都是通过 Object.defineProperty 进行的。而 loose mode 则更贴近 es5 的写法,性能更好一些,兼容性更好一些,但将这部份代码再转换成 native es6 的话会比较麻烦一些(感觉这一点并不是缺点,有源码就可以了)。

上面 esModule 解决的办法,实质就是将

改成 
exports.__esModule = true;

再举个例子,如下面的 Cat 类定义:

正常模式会编译为:

loose mode 模式会编译为:

babel es2015 中 loose 模式主要是针对下面几个 plugin:

  • transform-es2015-template-literals

  • transform-es2015-classes

  • transform-es2015-computed-properties

  • transform-es2015-for-of

  • transform-es2015-spread

  • transform-es2015-destructuring

  • transform-es2015-modules-commonjs

每一种的转换方式在此就不再赘述了,大家可以回家自己试。

如有错误,恳请斧正!

参考文章:

babel try out

template literals

block-scoped functions

classes

objects

commonjs and es6 module

原创文章转载请注明:

转载自AlloyTeam:http://www.alloyteam.com/2016/05/babel-code-into-a-bird-like/

  1. paranoidjk 2016 年 7 月 6 日

  2. zhang 2016 年 5 月 25 日

  3. evencom 2016 年 5 月 23 日

  4. 硬货 2016 年 5 月 23 日

    硬货

发表评论