如何读懂编译后的 JavaScript 代码

如何读懂编译后的 JavaScript 代码

Flying
2018-06-24 / 0 评论 / 526 阅读 / 正在检测是否收录...

JavaScript 代码为啥要编译?主要是采用 ES6 及以上语法编写的代码兼容不了老浏览器老环境。Bable 可以将其转换为向后兼容的 JavaScript 语法,以便能够运行在当前和旧版本的浏览器或其他环境中。Babel 通过插件让你现在就能使用新的语法,无需等待浏览器的支持。

lock-js.svg

安装 Babel

在我们安装了 @babel/cli@babel/core@babel/preset-env 和后,就可以使用 Babel 来转码了。

npm i -D @babel/cli @babel/core @babel/preset-env

转码分析

// test.js
// Babel 输入: ES2015 箭头函数
[1, 2, 3].map(n => n + 1);

上面代码执行 npx babel test.js -o test.es5.js 后转换为:

"use strict";

// Babel 输入: ES2015 箭头函数
[1, 2, 3].map(function (n) {
  return n + 1;
});

当然,这种简单语法,通过 Bable 官网的在线工具也能做到。Bable 编译后的 JavaScript 代码不难呀,您这样想就错了。实际项目可能很多文件很多模块,有可能还用 Webpack 等工具压缩混淆过,比这复杂多了。

实例源码

比如下面一个比较简单的 Node.js 项目。

Rectangle.js 代码

// Rectangle
class Rectangle {
  constructor(w, h) {
    this.w = w;
    this.h = h;
  }

  calcArea() {
    return this.w * this.h;
  }

  calcCircumference() {
    return 2 * this.w + this.h;
  }
}
module.exports = Rectangle;

main.js 代码

const express = require('express');
const Rectangle = require('./Rectangle.js');

const app = express();
const port = 3000;
app.get('/', (req, res) => {
  res.send(`The area is ${new Rectangle(10, 20).calcArea()}`);
})
app.listen(port, () => {
  console.log(`Example app listening on port ${port}`);
})

安装 babel-loader

Webpack 借助 babel-loader 实现了 babel 的代码转化功能,使用 terser-Webpack-plugin 实行代码合并压缩混淆。前者需要额外安装.

npm i -D babel-loader

后者在 Webpack 5 中已经集成,Webpack 4 中也需要单独安装,< Webpack 4 一般用它内置的 webpack.optimize.UglifyJsPlugin

初步分析

Node.js 代码可以直接用 node 命令来跑,即使是生产环境部署也可以这么做。但有时为了保护代码,生产环境下使用 Webpack 编译的“安全”代码。遇到这种代码,多数开发人员可能是就是看“天书”了。如下面格式化过的代码:

(() => {
  var e = {
    86: e => {
      function r(e) {
        return r = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function (e) {
          return typeof e
        } : function (e) {
          return e && "function" == typeof Symbol && e.constructor === Symbol && e !== Symbol.prototype ? "symbol" : typeof e
        }, r(e)
      }

      function t(e, t) {
        for (var o = 0; o < t.length; o++) {
          var n = t[o];
          n.enumerable = n.enumerable || !1, n.configurable = !0, "value" in n && (n.writable = !0), Object.defineProperty(e, (void 0, i = function (e, t) {
            if ("object" !== r(e) || null === e) return e;
            var o = e[Symbol.toPrimitive];
            if (void 0 !== o) {
              var n = o.call(e, "string");
              if ("object" !== r(n)) return n;
              throw new TypeError("@@toPrimitive must return a primitive value.")
            }
            return String(e)
          }(n.key), "symbol" === r(i) ? i : String(i)), n)
        }
        var i
      }

      var o = function () {
        function e(r, t) {
          !function (e, r) {
            if (!(e instanceof r)) throw new TypeError("Cannot call a class as a function")
          }(this, e), this.w = r, this.h = t
        }

        var r, o;
        return r = e, (o = [{
          key: "calcArea", value: function () {
            return this.w * this.h
          }
        }, {
          key: "calcCircumference", value: function () {
            return 2 * this.w + this.h
          }
        }]) && t(r.prototype, o), Object.defineProperty(r, "prototype", {writable: !1}), e
      }();
      e.exports = o
    }, 
    860: e => {
      "use strict";
      e.exports = require("express")
    }
  }, r = {};

  function t(o) {
    var n = r[o];
    if (void 0 !== n) return n.exports;
    var i = r[o] = {exports: {}};
    return e[o](i, i.exports, t), i.exports
  }

  var o, n, i, u = {};
  o = t(860), 
  n = t(86), 
  (i = o()).get("/", (function (e, r) {
   ...
})();

86086etr…… 一堆乱七八糟的变量,看不懂呀。别着急,慢慢分析。建议从后想前看就容易多了。

首先,注意到第 63t(860), 先看参数 860。它对应代码第 49~52 行,是一个对象字面量。其中 860 是键,值是一个函数,该函数导入了 express 模块,并通过 e.exports 导出。

在 Webpack中,对象 e 包含应用使用的所有模块,因为我们使用的是 CommonJS,能猜到是 Webpack 对 modules 实现。e.exports 实际上就是 module.exports

64t(86), 先看参数 86。它对应代码第 3~48行,也是一个对象字面量。其中 86 是键,值也是一个函数,该函数返回对类 Rectangle 引用,并通过 e.exports 导出。

然后,重点看类 Rectangle 对应的 ES5 实现。注意到第 29~46 行自执行函数 o 没有?它会返回自定义类 Rectangle,是基于 prototype的,只是基于 Object.defineProperty 包裹了一层:

Object.defineProperty(r, "prototype", {writable: !1})

自执行函数 o 在第 45 行使用 t(r.prototype, o) 调用了工具函数 t(e, t)。函数 t(e, t) 对应第 29~46 行代码,它 在第 16 行的 if 语句又调用了工具函数 r(e)。至于这两个工具函数,看到文章最后又清楚了。

对照分析

如果还是云里雾里的,还需“庖丁解牛”。接下来将 Webpack 的 optimization 配置如下:

optimization: {
  moduleIds: 'named', // 输出模块名
  minimize: false, // 不压缩
  minimizer: [new TerserPlugin({
    terserOptions: {
      format: {
        beautify: true, // 格式化
        comments: true // 保留注释
      }
    }
  })]
}

为了对照代码理解,此处我们会非压缩压缩以输出模块名,TerserPlugin 插件设置了输出代码格式化并保留注释。

最后,再次使用 Webpack 编译,生成的代码如下:

/******/ (() => { // WebpackBootstrap
/******/     var __Webpack_modules__ = ({

/***/ "./Rectangle.js":
/***/ ((module) => {

        function _typeof(obj) { "@babel/helpers - typeof"; return _typeof = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function (obj) { return typeof obj; } : function (obj) { return obj && "function" == typeof Symbol && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }, _typeof(obj); }
        function _classCallCheck(instance, Constructor) {
          if (!(instance instanceof Constructor)) {
            throw new TypeError("Cannot call a class as a function");
          }
        }
        function _defineProperties(target, props) {
          for (var i = 0; i < props.length; i++) {
            var descriptor = props[i];
            descriptor.enumerable = descriptor.enumerable || false;
            descriptor.configurable = true;
            if ("value" in descriptor) descriptor.writable = true;
            Object.defineProperty(target, _toPropertyKey(descriptor.key), descriptor);
          }
        }
        function _createClass(Constructor, protoProps, staticProps) {
          if (protoProps)
            _defineProperties(Constructor.prototype, protoProps);
          if (staticProps) _defineProperties(Constructor, staticProps);
          Object.defineProperty(Constructor, "prototype", { writable: false });
          return Constructor;
        }
        
        function _toPropertyKey(arg) {
          var key = _toPrimitive(arg, "string");
          return _typeof(key) === "symbol" ? key : String(key);
        }
        function _toPrimitive(input, hint) {
          if (_typeof(input) !== "object" || input === null) return input;
          var prim = input[Symbol.toPrimitive];
          if (prim !== undefined) {
            var res = prim.call(input, hint || "default");
            if (_typeof(res) !== "object") return res;
            throw new TypeError("@@toPrimitive must return a primitive value.");
          }
          return (hint === "string" ? String : Number)(input);
        }
        var Rectangle = /*#__PURE__*/function () {
          function Rectangle(w, h) {
            _classCallCheck(this, Rectangle);
            this.w = w;
            this.h = h;
          }
          _createClass(Rectangle, [{
            key: "calcArea",
            value: function calcArea() {
              return this.w * this.h;
            }
          }, {
            key: "calcCircumference",
            value: function calcCircumference() {
              return 2 * this.w + this.h;
            }
          }]);
          return Rectangle;
        }();
        module.exports = Rectangle;

        /***/
}),

/***/ "express":
/***/ ((module) => {

        "use strict";
        module.exports = require("express");

        /***/
})

    /******/
});
/************************************************************************/
/******/     // The module cache
/******/     var __Webpack_module_cache__ = {};
/******/
/******/     // The require function
/******/     function __Webpack_require__(moduleId) {
/******/         // Check if module is in cache
/******/         var cachedModule = __Webpack_module_cache__[moduleId];
/******/         if (cachedModule !== undefined) {
/******/             return cachedModule.exports;
      /******/
}
/******/         // Create a new module (and put it into the cache)
/******/         var module = __Webpack_module_cache__[moduleId] = {
/******/             // no module.id needed
/******/             // no module.loaded needed
/******/             exports: {}
      /******/
};
/******/
/******/         // Execute the module function
/******/         __Webpack_modules__[moduleId](module, module.exports, __Webpack_require__);
/******/
/******/         // Return the exports of the module
/******/         return module.exports;
    /******/
}
  /******/
  /************************************************************************/
  var __Webpack_exports__ = {};
  // This entry need to be wrapped in an IIFE because it need to be isolated against other modules in the chunk.
  (() => {
    var express = __Webpack_require__("express");
    var Rectangle = __Webpack_require__("./Rectangle.js");
    var app = express();
    var port = 3000;
    app.get('/', function (req, res) {
      // 本生成的是concat,主机会报错,故用 + 代替
      res.send('The area is ' + new Rectangle(10, 20).calcArea());
    });
    app.listen(port, function () {
      console.log("Example app listening on port ".concat(port));
    });
  })();

  var __Webpack_export_target__ = exports;
  for (var i in __Webpack_exports__) __Webpack_export_target__[i] = __Webpack_exports__[i];
  if (__Webpack_exports__.__esModule) Object.defineProperty(__Webpack_export_target__, "__esModule", { value: true });
  /******/
})()
  ;

这段编译过的代码没有压缩混淆,和开发环境的代码差不多。可以看到,moduleIds 已经是模块名 ./Rectangle.js" 和 "express",比原来的86086 好理解多了。对象 e 确实对应 module, 而函数 Rectangle 的确对应混淆过的类型 o

工具函数 _defineProperties 对应混淆过的函数 t, 工具函数 _typeof 对应混淆过的函数 _typeof。其他工具函数如 _classCallCheck_createClass_toPropertyKey_toPrimitive 在本应用中只会调用一次,没有 TreeShaking 的价值。于是 Webpack 将就它们混淆成了匿名函数,这样性能会好些。

只要是用到 Webpack + Bable + Class 的应用,生成的 ES5 buldle 都会 _defineProperties_classCallCheck_createClass三个工具函数。本实例使用的是 Bable 7,如果使用 Bable 6, defineProperties 函数是放在 _createClass 函数内部的。

还有一些 Webpack 相关的通用代码,比如,__Webpack_modules___Webpack_module_cache____Webpack_require____Webpack_export_target__。参看注释,应该能懂吧?

参考项目

访问 codesandbox 查看完整项目代码

纯 JavaScript 项目还好,如果使用了React、Vue等框架,问题比这复杂多了。我在这里只是抛砖引玉,路漫漫呀,下次分解。

5

评论 (0)

取消