写这篇分析是为了追踪 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
'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]);参考链接