babel 到底將代碼轉換成什麼鳥樣?

原文連接:https://github.com/lcxfs1991/blog/issues/9javascript

前言

將babel捧做前端一個劃時代的工具必定也不爲過,它的出現讓許多程序員幸福地用上了es6新語法。但你就這麼放心地讓babel跑在外網?反正我是不放心,我就曾通過被坑過,因而萌生了研究babel代碼轉換的想法。本文不是分析babel源碼,僅僅是看看babel轉換的最終產物。php

es6在babel中又稱爲es2015。因爲es2015語法衆多,本文僅挑選了較爲經常使用的一些語法點,並且主要是分析babel-preset-2015這個插件(react開發的時候,常在webpack中用到這個preset)。html

babel-preset-2015

打開babel-preset2015插件一看,一共20個插件。熟悉es2015語法的同志一看,多多少少能從字面意思知道某個插件是用於哪一種語法的轉換前端

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

  • babel-plugin-transform-es2015-literalsreact

  • babel-plugin-transform-es2015-function-name => 函數name屬性android

  • babel-plugin-transform-es2015-arrow-functions => 箭頭函數webpack

  • babel-plugin-transform-es2015-block-scoped-functions => 函數塊級做用域git

  • 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編譯會直接報錯。
轉換前

var a = 1;
let b = 2;
const c = 3;

轉換後:

var a = 1;
var b = 2;
var c = 3;

那let的塊級做用怎麼體現呢?來看看下面例子,實質就是在塊級做用改變一下變量名,使之與外層不一樣。
轉換前:

let a1 = 1;
let a2 = 6;

{
    let a1 = 2;
    let a2 = 5;

    {
        let a1 = 4;
        let a2 = 5;
    }
}
a1 = 3;

轉換後:

var a1 = 1;
var a2 = 6;

{
    var _a = 2;
    var _a2 = 5;

    {
        var _a3 = 4;
        var _a4 = 5;
    }
}
a1 = 3;

賦值解構

寫react的時候,咱們使用負值解構去取對象的值,用起來很是爽,像這樣:

var props = {
    name: "heyli",
    getName: function() {

    },
    setName: function() {

    }
};

let { name, getName, setName } = this.props;

咱們來看看轉換的結果:

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

var name = props.name;
var getName = props.getName;
var setName = props.setName;

至於數組呢?若是是一個匿名數組,則babel會幫你先定義一個變量存放這個數組,而後再對須要賦值的變量進行賦值。
轉換前:

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

轉換後:

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

看到這個,感受轉換結果跟咱們想的還蠻一致。哈哈,使用的噩夢還沒開始。

若是使用匿名對象直接進行賦值解構會怎樣呢?以下。babel爲了使接收的變量惟一,直接就將匿名對象裏的屬性拼在一塊兒,組成接收這個匿名對象的變量,嚇得我趕忙檢查一下項目裏有沒有這種寫法。
轉換前:

var { abc, bcd, cde, def } = { "abc": "abc", "bcd": "bcd", "cde": "cde", "def": "def", "efg": "efg", "fgh": "fgh" };

轉換後:

var _abc$bcd$cde$def$efg$ = { "abc": "abc", "bcd": "bcd", "cde": "cde", "def": "def", "efg": "efg", "fgh": "fgh" };
var abc = _abc$bcd$cde$def$efg$.abc;
var bcd = _abc$bcd$cde$def$efg$.bcd;
var cde = _abc$bcd$cde$def$efg$.cde;
var def = _abc$bcd$cde$def$efg$.def;

還有一種對象深層次的解構賦值:
轉換前:

var obj = {
    p1: [
        "Hello",
        { p2: "World" }
      ]
};

var { p1: [s1, { p2 }] } = obj;

轉換後:

// 爲解釋本人將代碼美化了
var _slicedToArray = (function() {
    function sliceIterator(arr, i) {
        var _arr = [];
        var _n = true;
        var _d = false;
        var _e = undefined;
        try {
           // 用Symbol.iterator造了一個可遍歷對象,而後進去遍歷。
            for (var _i = arr[Symbol.iterator](), _s; !(_n = (_s = _i.next()).done); _n = true) {
                _arr.push(_s.value);
                if (i && _arr.length === i) break;
            }
        } catch (err) {
            _d = true;
            _e = err;
        } finally {
            try {
                if (!_n && _i["return"]) _i["return"]();
            } finally {
                if (_d) throw _e;
            }
        }
        return _arr;
    }
    return function(arr, i) {
        if (Array.isArray(arr)) {
            return arr;
        } else if (Symbol.iterator in Object(arr)) {
            return sliceIterator(arr, i);
        } else {
            throw new TypeError("Invalid attempt to destructure non-iterable instance");
        }
    };
})();

var obj = {
   p1: ["Hello", { p2: "World" }]
};

var _obj$p = _slicedToArray(obj.p1, 2);

var s1 = _obj$p[0];
var p2 = _obj$p[1].p2;

babel在代碼頂部生產了一個公共的代碼_slicedToArray。大概就是將對象裏面的一些屬性轉換成數組,方便解構賦值的進行。但Symbol.iterator的兼容性並很差(以下圖),仍是謹慎使用爲妙。

圖片描述

另外,下面這種對字符串進行賦值解構也一樣使用到_slicedToArray方法:

const [a, b, c, d, e] = 'hello';

函數參數默認值及擴展運算符

在es5的年代,通常咱們寫參數的默認值都會這麼寫:

function func(x, y) {
    var x = x || 1;
    var y = y || 2;
}

咱們來看看babel的轉換辦法:

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 ? { x: 0, y: 0 } : arguments[0];

  var x = _ref.x;
  var y = _ref.y;

  return [x, y];
}

function func1() {
  var x = arguments.length <= 0 || arguments[0] === undefined ? 1 : arguments[0];
  var y = arguments.length <= 1 || arguments[1] === undefined ? 2 : arguments[1];

  return [x, y];
}

babel這裏使有了arguments來作判。第一種狀況涉及解構賦值,所以x和y的值仍是有多是undefined的。至於第二種狀況,則會保證2個參數的默認值分別是1和2.

再來看一種。...y表明它接收了剩下的參數。也就是arguments除了第一個標號的參數以外剩餘的參數。

轉換前:

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 = Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) {
        y[_key - 1] = arguments[_key];
    }

    console.log(y);
    return x * y.length;
}

箭頭函數

剪頭函數其實主要是省了寫函數的代碼,同時可以直接用使外層的this而不用擔憂context切換的問題。之前咱們通常都要在外層多寫一個_this/self直向this。babel的轉換辦法其實跟咱們的處理無異。

轉換前:

var obj = {
    prop: 1,
    func: function() {
        var _this = this;

        var innerFunc = () => {
            this.prop = 1;
        };

        var innerFunc1 = function() {
            this.prop = 1;
        };
    },

};

轉換後:

var obj = {
    prop: 1,
    func: function func() {
        var _this2 = this;

        var _this = this;

        var innerFunc = function innerFunc() {
            _this2.prop = 1;
        };

        var innerFunc1 = function innerFunc1() {
            this.prop = 1;
        };
    }

};

對象的能力加強

對象屬性的快捷定義

轉換前:

var a = 1,
    b = "2",
    c = function() {
        console.log('c');
    };

var obj = {a, b, c};

轉換後:

var a = 1,
    b = "2",
    c = function c() {
    console.log('c');
};

var obj = { a: a, b: b, c: c };

對象中括號屬性

es2015開始新增了在對象中用中括號解釋屬性的功能,這對變量、常量等當對象屬性尤爲有用。

轉換前:

const prop2 = "PROP2";
var obj = {
    ['prop']: 1,
    ['func']: function() {
        console.log('func');
    },
        [prop2]: 3
};

轉換後:

var _obj;
// 已美化
function _defineProperty(obj, key, value) {
    if (key in obj) {
        Object.defineProperty(obj, key, {
            value: value,
            enumerable: true,
            configurable: true,
            writable: true
        });
    } else {
        obj[key] = value;
    }
    return obj;
}

var prop2 = "PROP2";
var obj = (_obj = {}, _defineProperty(_obj, 'prop', 1), _defineProperty(_obj, 'func', function func() {
    console.log('func');
}), _defineProperty(_obj, prop2, 3), _obj);

看似簡單的屬性,babel卻大動干戈。新增了一個_defineProperty函數,給新建的_obj = {}進行屬性定義。除此以外使用小括號包住一系列從左到右的運算使整個定義更簡潔。

使用super去調用prototype

之前咱們通常都用obj.prototype或者嘗試用this去往上尋找prototype上面的方法。而babel則本身寫了一套在prototype鏈上尋找方法/屬性的算法。

轉換前:

var obj = {
    toString() {
     // Super calls
     return "d " + super.toString();
    },
};

轉換後:

var _obj;
// 已美化
var _get = function get(object, property, receiver) {
   // 若是prototype爲空,則往Function的prototype上尋找
    if (object === null) object = Function.prototype;
    var desc = Object.getOwnPropertyDescriptor(object, property);
    if (desc === undefined) {
        var parent = Object.getPrototypeOf(object);
        // 若是在本層prototype找不到,再往更深層的prototype上找
        if (parent === null) {
            return undefined;
        } else {
            return get(parent, property, receiver);
        }
    }
    // 若是是屬性,則直接返回
    else if ("value" in desc) {
        return desc.value;
    }
    // 若是是方法,則用call來調用,receiver是調用的對象 
    else {
        var getter = desc.get;  // getOwnPropertyDescriptor返回的getter方法
        if (getter === undefined) {
            return undefined;
        }
        return getter.call(receiver);
    }
};

var obj = _obj = {
  toString: function toString() {
    // Super calls
    return "d " + _get(Object.getPrototypeOf(_obj), "toString", this).call(this);
  }
};

Object.assign 和 Object.is

es6新增的Object.assign極大方便了對象的克隆複製。但babel的es2015 preset並不支持,因此沒對其進入轉換,這會使得一些移動端機子遇到這種寫法會報錯。因此通常開發者都會使用object-assign這個npm的庫作兼容。

Object.is用於比較對象的值與類型,es2015 preset一樣不支持編譯。

es6模板

多行字符串

轉換前:

console.log(`string text line 1
string text line 2`);

轉換後:

console.log("string text line 1\nstring text line 2");

字符中變量運算

轉換前:

var a = 5;
var b = 10;
console.log(`Fifteen is ${a + b} and not ${2 * a + b}.`);

轉換後:

var a = 5;
var b = 10;
console.log("Fifteen is " + (a + b) + " and not " + (2 * a + b) + ".");

標籤模板

es6的這種新特性給模板處理賦予更強大的功能,一改以往對模板進行各類replace的處理辦法,用一個統一的handler去處理。babel的轉換主要是添加了2個屬性,所以看起來也並不算比較工程浩大的編譯。

轉換前:

var a = 5;
var b = 10;

function tag(strings, ...values) {
  console.log(strings[0]); // "Hello "
  console.log(strings[1]); // " world "
  console.log(values[0]);  // 15
  console.log(values[1]);  // 50

  return "Bazinga!";
}

tag`Hello ${ a + b } world ${ a * b }`;

轉換後:

var _templateObject = _taggedTemplateLiteral(["Hello ", " world ", ""], ["Hello ", " world ", ""]);
// 已美化
function _taggedTemplateLiteral(strings, raw) {
    return Object.freeze(Object.defineProperties(strings, {
        raw: {
            value: Object.freeze(raw)
        }
    }));
}
// 給傳入的object定義strings和raw兩個不可變的屬性。

var a = 5;
var b = 10;

function tag(strings) {
  console.log(strings[0]); // "Hello "
  console.log(strings[1]); // " world "
  console.log(arguments.length <= 1 ? undefined : arguments[1]); // 15
  console.log(arguments.length <= 2 ? undefined : arguments[2]); // 50

  return "Bazinga!";
}

tag(_templateObject, a + b, a * b);

模塊化與類

類class

javascript實現oo一直是很是熱門的話題。從最原始時代須要手動維護在構造函數裏調用父類構造函數,到後來封裝好函數進行extend繼承,再到babel出現以後能夠像其它面向對象的語言同樣直接寫class。es2015的類方案仍然算是過渡方案,它所支持的特性仍然沒有涵蓋類的全部特性。目前主要支持的有:

  • constructor

  • static方法

  • get 方法

  • set 方法

  • 類繼承

  • super調用父類方法。

轉換前:

class Animal {

    constructor(name, type) {
        this.name = name;
        this.type = type;
    }

    walk() {
        console.log('walk');
    }

    run() {
        console.log('run')
    }

    static getType() {
        return this.type;
    }

    get getName() {
        return this.name;
    }

    set setName(name) {
        this.name = name;
    }


}

class Dog extends Animal {
    constructor(name, type) {
        super(name, type);
    }

    get getName() {
        return super.getName();
    }
}

轉換後(因爲代碼太長,先省略輔助的方法):

/**
......一堆輔助方法,後文詳述
**/
var Animal = (function () {
    function Animal(name, type) {
                // 此處是constructor的實現,用_classCallCheck來斷定constructor正確與否
        _classCallCheck(this, Animal);

        this.name = name;
        this.type = type;
    }
        // _creatClass用於建立類及其對應的方法
    _createClass(Animal, [{
        key: 'walk',
        value: function walk() {
            console.log('walk');
        }
    }, {
        key: 'run',
        value: function run() {
            console.log('run');
        }
    }, {
        key: 'getName',
        get: function get() {
            return this.name;
        }
    }, {
        key: 'setName',
        set: function set(name) {
            this.name = name;
        }
    }], [{
        key: 'getType',
        value: function getType() {
            return this.type;
        }
    }]);

    return Animal;
})();

var Dog = (function (_Animal) {
        // 子類繼承父類
    _inherits(Dog, _Animal);

    function Dog(name, type) {
        _classCallCheck(this, Dog);
                // 子類實現constructor
                // babel會強制子類在constructor中使用super,不然編譯會報錯
        return _possibleConstructorReturn(this, Object.getPrototypeOf(Dog).call(this, name, type));
    }

    _createClass(Dog, [{
        key: 'getName',
        get: function get() {
                       // 跟上文使用super調用原型鏈的super編譯解析的方法一致,
                       // 也是本身寫了一個回溯prototype原型鏈
            return _get(Object.getPrototypeOf(Dog.prototype), 'getName', this).call(this);
        }
    }]);

    return Dog;
})(Animal);
// 檢測constructor正確與否
function _classCallCheck(instance, Constructor) {
    if (!(instance instanceof Constructor)) {
        throw new TypeError("Cannot call a class as a function");
    }
}
// 建立類
var _createClass = (function() {
    function defineProperties(target, props) {
        for (var i = 0; i < props.length; i++) {
            var descriptor = props[i];
            // es6規範要求類方法爲non-enumerable
            descriptor.enumerable = descriptor.enumerable || false;
            descriptor.configurable = true;
            // 對於setter和getter方法,writable爲false
            if ("value" in descriptor) descriptor.writable = true;
            Object.defineProperty(target, descriptor.key, descriptor);
        }
    }
    return function(Constructor, protoProps, staticProps) {
        // 非靜態方法定義在原型鏈上
        if (protoProps) defineProperties(Constructor.prototype, protoProps);
        // 靜態方法直接定義在constructor函數上
        if (staticProps) defineProperties(Constructor, staticProps);
        return Constructor;
    };
})();
// 繼承類
function _inherits(subClass, superClass) {
   // 父類必定要是function類型
    if (typeof superClass !== "function" && superClass !== null) {
        throw new TypeError("Super expression must either be null or a function, not " + typeof superClass);
    }
    // 使原型鏈subClass.prototype.__proto__指向父類superClass,同時保證constructor是subClass本身
    subClass.prototype = Object.create(superClass && superClass.prototype, {
        constructor: {
            value: subClass,
            enumerable: false,
            writable: true,
            configurable: true
        }
    });
    // 保證subClass.__proto__指向父類superClass
    if (superClass) 
        Object.setPrototypeOf ? 
        Object.setPrototypeOf(subClass, superClass) :    subClass.__proto__ = superClass;
}
// 子類實現constructor
function _possibleConstructorReturn(self, call) {
    if (!self) {
        throw new ReferenceError("this hasn't been initialised - super() hasn't been called");
    }
    // 若call是函數/對象則返回
    return call && (typeof call === "object" || typeof call === "function") ? call : self;
}

先前在用react重構項目的時候,全部的react組件都已經摒棄了es5的寫法,一概採用了es6。用類的好處寫繼續更加方便,但沒法用mixin,須要藉助更新的es7語法中的decorator纔可以實現類mixin的功能(例如pureRender)。但此次分析完babel源碼以後,才發現原來babel在實現class特性的時候,定義了許多方法,儘管看起來並不太優雅。

模塊化

在開發react的時候,咱們每每用webpack搭配babel的es2015和react兩個preset進行構建。以前看了一篇文章對babel此處的模塊加載有些啓發(《分析 Babel 轉換 ES6 module 的原理》)。

示例:

// test.js
import { Animal as Ani, catwalk } from "./t1";
import * as All from "./t2";


class Cat extends Ani {

    constructor() {
        super();
    }
}

class Dog extends Ani {
    constructor() {
        super();
    }
}
// t1.js
export class Animal {

    constructor() {

    }

}

export function catwal() {
    console.log('cat walk');
};
// t2.js
export class Person {
    constructor() {

    }

}

export class Plane {
    constructor() {

    }

}

經過webpack與babel編譯後:

// t1.js的模塊
Object.defineProperty(exports, "__esModule", {
    value: true
});
exports.catwal = catwal;

// 省略一些類繼承的方法

var Animal = exports.Animal = function Animal() {
    _classCallCheck(this, Animal);
};

function catwal() {
    console.log('cat walk');
};

// t2.js的模塊
Object.defineProperty(exports, "__esModule", {
    value: true
});

// 省略一些類繼承的方法

var Person = exports.Person = function Person() {
    _classCallCheck(this, Person);
};

var Plane = exports.Plane = function Plane() {
    _classCallCheck(this, Plane);
};

// test.js的模塊
var _t = __webpack_require__(1);

var _t2 = __webpack_require__(3); // 返回的都是exports上返回的對象屬性

var All = _interopRequireWildcard(_t2);

function _interopRequireWildcard(obj) {
    // 發現是babel編譯的, 直接返回
    if (obj && obj.__esModule) {
        return obj;
    }
   // 非babel編譯, 猜想多是第三方模塊,爲了避免報錯,讓default指向它本身
    else {
        var newObj = {};
        if (obj != null) {
            for (var key in obj) {
                if (Object.prototype.hasOwnProperty.call(obj, key)) newObj[key] = obj[key];
            }
        }
        newObj.default = obj;
        return newObj;
    }
}

// 省略一些類繼承的方法

var Cat = (function (_Ani) {
    _inherits(Cat, _Ani);

    function Cat() {
        _classCallCheck(this, Cat);

        return _possibleConstructorReturn(this, Object.getPrototypeOf(Cat).call(this));
    }

    return Cat;
})(_t.Animal);

var Dog = (function (_Ani2) {
    _inherits(Dog, _Ani2);

    function Dog() {
        _classCallCheck(this, Dog);

        return _possibleConstructorReturn(this, Object.getPrototypeOf(Dog).call(this));
    }

    return Dog;
})(_t.Animal);

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解決的辦法,實質就是將

Object.defineProperty(exports, "__esModule", {
    value: true
});

改爲 exports.__esModule = true;

再舉個例子,以下面的Cat類定義:

class Cat extends Ani {

    constructor() {
        super();
    }

    miao() {
        console.log('miao');
    }
}

正常模式會編譯爲:

var Cat = (function (_Ani) {
    _inherits(Cat, _Ani);

    function Cat() {
        _classCallCheck(this, Cat);

        return _possibleConstructorReturn(this, Object.getPrototypeOf(Cat).call(this));
    }

    _createClass(Cat, [{
        key: "miao",
        value: function miao() {
            console.log('miao');
        }
    }]);

    return Cat;
})(_t.Animal);

loose mode模式會編譯爲:

var Cat = (function (_Ani) {
    _inherits(Cat, _Ani);

    function Cat() {
        _classCallCheck(this, Cat);

        return _possibleConstructorReturn(this, _Ani.call(this));
    }

    Cat.prototype.miao = function miao() {
        console.log('miao');
    };

    return Cat;
})(_t.Animal);

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

相關文章
相關標籤/搜索