写这篇分析是为了追踪 Babel 的一处模块化转换 bug,暂时还没有理清头绪,所以写下此详细分析,等分析出结果再更新此文。
以下内容仅讨论 Babel 与 CMD 模块风格,当然 Babel 也可以使用 AMD 风格转换模块。
模块导出
export const InlineExport = { } const NormalExport = { } const RenameExport = { } const DefaultExport = { } export { NormalExport } export { RenameExport as HasRenamed } export default DefaultExport // 转换后 "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); var InlineExport = exports.InlineExport = {}; var NormalExport = {}; var RenameExport = {}; var DefaultExport = {}; exports.NormalExport = NormalExport; exports.HasRenamed = RenameExport; exports.default = DefaultExport;
模块引入
import { NormalExport } from 'normal' import { HasRenamed as RenameAgain } from 'rename' import DefaultExport from 'default' import * as All from 'all' NormalExport() RenameAgain() DefaultExport() All() // 转换后 'use strict'; var _normal = require('normal'); var _rename = require('rename'); var _default = require('default'); var _default2 = _interopRequireDefault(_default); var _all = require('all'); var all = _interopRequireWildcard(_all); (0, _normal.NormalExport)(); (0, _rename.HasRenamed)(); (0, _default2.default)(); all.hello(); function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } 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; } }
代码非常易读,但是有两个疑问
- 模块导出时通过
Object.defineProperty
定义的exports.__esModule
有什么用? - 模块引入后执行时为什么使用逗号表达式?
要解开这两个疑惑,必须得理解 ES6 的模块化方案和 CommonJS 方案有什么区别与联系。
理解 ES6 module 和 CommonJS 的区别
先说说 CommonJS 方案的特点:
- 所有要输出的对象统统挂载在
module.exports
上,然后暴露给外界 - 通过
require
加载别的模块,require
的返回值就是模块暴露的对象 - CommonJS 是一个单对象输出,单对象加载的模型
再来看看 ES6 的模块化机制有什么特点:
- 可通过以下方式输出任何对模块内部的引用
export { A, B }
export { A as a, B }
export default A
export const A = { }
- 通过以下方式加载模块中输出的任意引用
import A from './module'
import * as A from './module'
import { A, B } from './module'
import { A as a, B } from './module'
- ES6 module 是一个多对象输出,多对象加载的模型
理解 ES6 module 和 CommonJS 的联系
目前的浏览器几乎都不支持 ES6 的模块机制,所以我们要用 Babel 把 ES6 的模块机制转换成 CommonJS 的形式,然后使用 Browserify 或者 Webpack 这样的打包工具把他们打包起来(本文结束的地方会给出一个 Browserify 打包后的代码示例,不熟悉的话看上去会比较凌乱)。
然后问题就来了,ES6 的模块机制和 CommonJS 机制差距甚大,所以 Babel 需要在借助 CommonJS 的实现基础上稍作修改,以达到符合 ES6 标准的目的。
于是 Babel 开始发挥奇技淫巧了 ~
Babel 是怎么实现 ES6 模块的转换的?
本文刚开篇给出的两端代码就是 Babel 的转换方法,现在我来解释 Babel 是怎么做的。
Babel 依然通过 exports
对象来输出模块内的引用,但是增加了一个特殊的 exports.default
属性用来实现 ES6 的默认输出对象。并且依然通过 require
来实现模块的加载。
疑问一的答案
给模块的输出对象增加 __esModule
是为了将不符合 Babel 要求的 CommonJS 模块转换成符合要求的模块,这一点在 require
的时候体现出来。如果加载模块之后,发现加载的模块带着一个 __esModule
属性,Babel 就知道这个模块肯定是它转换过的,这样 Babel 就可以放心地从加载的模块中调用 exports.default
这个导出的对象,也就是 ES6 规定的默认导出对象,所以这个模块既符合 CommonJS 标准,又符合 Babel 对 ES6 模块化的需求。然而如果 __esModule
不存在,也没关系,Babel 在加载了一个检测不到 __esModule
的模块时,它就知道这个模块虽然符合 CommonJS 标准,但可能是一个第三方的模块,Babel 没有转换过它,如果以后直接调用 exports.default
是会出错的,所以现在就给它补上一个 default
属性,就干脆让 default
属性指向它自己就好了,这样以后就不会出错了。
疑问二的答案
这个逗号表达式是 JavaScript 的语言特性,具体含义是这样的:整个逗号表达式都会从左到右执行一遍,然后逗号表达式的值等于最后一个逗号之后的表达式的值。这跟 C/C++ 等其他 C 类语言是一样的。
但是这句话对 JavaScript 有个特殊含义,如果执行 (0, foo.bar)()
,这个逗号表达式等价于执行 foo.bar()
,但是执行时的上下文环境会被绑定到全局对象身上,所以实际上真正等价于执行 foo.bar.call(GLOBAL_OBJECT)
。
前面提到的打包后的代码示例
(function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o<r.length;o++)s(r[o]);return s})({1:[function(require,module,exports){ 'use strict'; var _lib = require('./lib.js'); var _lib2 = _interopRequireDefault(_lib); function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } if (window && window.alert) { alert(_lib2.default); } else { console.log(_lib2.default); } },{"./lib.js":2}],2:[function(require,module,exports){ 'use strict'; Object.defineProperty(exports, "__esModule", { value: true }); var _classCallCheck = require('babel-runtime/helpers/classCallCheck'); var _classCallCheck2 = _interopRequireDefault(_classCallCheck); function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } exports.default = 'hello, world'; },{"babel-runtime/helpers/classCallCheck":3}],3:[function(require,module,exports){ "use strict"; exports.__esModule = true; exports.default = function (instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }; },{}]},{},[1]);
参考链接