[ babel 转换过程以及 es6 新语法转换后的样子 | 青训营笔记 ]

96 阅读6分钟

1. babel 转换过程

2f90236f5c914a069bd51611b75160a7~tplv-k3u1fbpfcp-zoom-in-crop-mark:4536:0:0:0.awebp (2616×1106) (byteimg.com)

  1. Parsing 解析

这个过程要经 词法分析语法分析构建AST(抽象语法树) 一系列操作;

词法分析: 使用 tokenizer(分词器) 或者 lexer(词法分析器),将源码拆分成 tokens,tokens 是一个放置对象的数组,其中的每一个对象都可以看做是一个单元(数字,标签,标点,操作符...)的描述信息。

❌ 词法分析不能帮助我们判断该条语句是否合法 👉 语法解析

语法解析: 将 tokens 重新整理成语法相互关联的表达形式 ,这种表达形式一般被称为 中间层或者AST(抽象语法树)

  1. Transformation 转换

改写AST(抽象语法树)或者根据当前AST(抽象语法树)生成一个新的AST(抽象语法树),这个过程可以是相同语言,或者可以直接将AST(抽象语法树)翻译为其他语言。

需要遍历这个“树”的节点 并读取其内容,由此引出 Traversal(遍历)Visitors (访问器)

Traversal(遍历) :遍历 AST 的所有节点,这个过程使用深度优先原则

Visitors (访问器) :访问器最基本的思想是创建一个“访问器”对象,这个对象可以处理不同类型的节点函数

  1. Code Generation 生成代码

将生成的新 AST 树再转回代码的过程。大部分的代码生成器主要过程是,不断的访问 Transformation 生成的 AST (抽象语法树)或者再结合 tokens,按照指定的规则,将“树”上的节点打印拼接最终还原为新的 code

2. babel 插件

  • @babel/parser 可以把源码转换成AST
  • @babel/traverse 用于对 AST 的遍历,维护了整棵树的状态,并且负责替换、移除和添加节点
  • @babel/generate 可以把AST生成源码,同时生成sourcemap
  • @babel/types 用于 AST 节点的 Lodash 式工具库, 它包含了构造、验证以及变换 AST 节点的方法,对编写处理 AST 逻辑非常有用
  • @babel/core Babel 的编译器,核心 API 都在这里面,比如常见的 transformparse,并实现了插件功能

webpack 配置 babel

  • babel-loader:架起连接 webpack 和 babel 的桥梁
  • @babel/preset-env 将 ES6 语法转成 ES5
  • @babel/polyfill 使低版本浏览器也支持所有ES6的语法

3. es6 的一些新语法转 es5

3.1 const、let

ES5 中的变量只有全局作用域函数作用域,某些场景会出现下列问题:

  • 内层变量会覆盖外层变量
  • 用于计数的循环(for循环)变量泄露为全局变量

📚 const、let、var 的区别?

  1. 变量提升

    • 『var 声明的变量存在变量提升』,变量可以在声明之前调用,值为 undefined
    • 『let 和 const 不存在变量提升』,声明的变量一定要在声明后使用,否则报错
  2. 暂时性死区

    • 『let 和 const 存在暂时性死区』,变量在定义语句之前,如果使用会抛出错误
  3. 块级作用域

    • var 全局作用域和函数作用域
    • let 和 const 存在块级作用域
  4. 重复声明

    • let 和 const 不允许重复声明(会抛出错误)
  5. 修改声明的变量

    • var 和 let 可以
    • const 声明一个只读的常量,一旦声明,基本数据类型的值就不能改变

es6 转换前

let a = '1'
let b = [1, 2, 3, 4]
for(let a in b){
   console.log(b[a]) 
}
const c = 2

转换后

var a = '1';
var b = [1, 2, 3, 4];
for (var _a in b) {
  console.log(b[_a]);
}
var c = 2;

🤔 块级作用域如何实现?

在块级作用域内改变一下变量名,使之与外层不同。

3.2 模板字符串

(``) 基本的字符串格式化,将表达式嵌入字符串中进行拼接,用 ${} 来定义

es6 转换前

let a = 1
let b = [1,2,3,4]
const c = `${b}包含${a}`

转换后

var a = 1;
var b = [1, 2, 3, 4];
var c = b + "\u5305\u542B" + a;

3.3 解构赋值

  1. 对象

转换前

var props = {
    name: "heyli",
    getName: function() {
​
    },
    setName: function() {
​
    }
};
​
let { name, getName, setName } = props;

转换后

var props = {
  name: "heyli",
  getName: function getName() {},
  setName: function setName() {}
};
var name = props.name,
  getName = props.getName,
  setName = props.setName;
  1. 数组

『匿名数组』

转换前

var [ a1, a2 ] = [1, 2, 3];

转换后

var _ref = [1, 2, 3],
  a1 = _ref[0],
  a2 = _ref[1];
  1. 对象深层次解构赋值、字符串解构赋值

babel 在代码顶部生产了一个公共的代码 _slicedToArray。大概过程是『将对象里面的一些属性转换成数组』,方便解构赋值的进行。

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

使用 argument 来做判断

第一种情况: 涉及对象的解构赋值,x 和 y 的值有可能是 undefined。所以这里需要当 arguments 的长度大于 0 而且 arguments[0] 不为 undefined 时,才对 arguments[0] 进行解构赋值

第二种情况: 保证分别存在第一个和第二个参数以及参数的值不为 undefined 时,进行赋值,否则为默认值。

转换前

function func({x, y} = { x: 0, y: 0 }) {
  return [x, y];
}
function func1(x = 1, y = 2) {
    return [x, y];
}

转换后

function func() {
  var _ref = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {
      x: 0,
      y: 0
    },
    x = _ref.x,
    y = _ref.y;
  return [x, y];
}
function func1() {
  var x = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : 1;
  var y = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 2;
  return [x, y];
}

第三种情况: 扩展运算符,...y 代表 y 接收了 argument 除了第一个参数之外的所有参数,转换后是使用 for 循环为数组 y 设置值。

转换前

function func(x, ...y) {
    console.log(x);
    console.log(y);
    return x * y.length;
}

转换后

function func(x) {
  console.log(x);
  for (var _len = arguments.length, y = new Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) {
    y[_key - 1] = arguments[_key];
  }
  console.log(y);
  return x * y.length;
}

3.5 箭头函数

📚 箭头函数与普通函数的区别

  1. 箭头函数的语法更加简洁

  2. 箭头函数没有自己的 this,会捕获其所在上下文的 this,作为自己的 this。

  3. call()、apply()、bind() 等方法都不能改变箭头函数中的 this 指向

  4. 箭头函数 this 指向无法改变,所以也不可以作为构造函数,不可以使用 new 命令。

  5. 箭头函数没有自己的 arguments 对象,在箭头函数里面访问到的 arguments 实际上是外层函数的 arguments 值。可以用 rest 参数解决。

    const add = (...args) => {
        console.log(args.reduce((pre, cur) => pre + cur)) 
    }
    add(1, 2, 3) // 6
    
  6. 箭头函数没有原型对象 prototype

  7. 箭头函数不能使用 yield 关键字,不能用作 Generator 函数

转换前

var obj = {
    prop: 1,
    func: function() {
        var innerFunc = () => {
            this.prop = 1;
        };
        var innerFunc1 = function() {
            this.prop = 1;
        };
    },
}

转换后

var obj = {
  prop: 1,
  func: function func() {
    var _this = this;
    var innerFunc = function innerFunc() {
      _this.prop = 1;
    };
    var innerFunc1 = function innerFunc1() {
      this.prop = 1;
    };
  }
};

箭头函数转 es5 语法: 将简洁的语法补全,同时会在外层多写一个 _this = this 使其指向箭头函数所在上下文的 this。

👇 参考:

  1. 前端工程化基石 -- AST(抽象语法树)以及AST的广泛应用🔥 - 掘金 (juejin.cn)
  2. babel 到底将代码转换成什么鸟样? - 掘金 (juejin.cn)