这个两个规范曾经困扰过我,由于他们的关键词都很像:import/export/export default/require/module.exports... 老是傻傻分不清楚。javascript
关于他们之间的区别只是据说一个动态加载、一个静态加载;一个导出副本,一个导出引用。这又是什么意思,为何?java
为了解惑,我使用了webpack+babel将这两种规范的模块代码打包成ES5,看看他们到底都是怎么作的。本文先解释他们各自的概念、表现,再来分析代码、剖析原理。(使用babel主要是为了把ES6 Module转成ES5看它内部是怎么工做的(更新:webpack原生支持import/export,无需使用babel转译))node
若是不采用模块化,从body底部引入js文件时必需要确保引用顺序正确,不然将没法运行。而当js文件数量太大的时候,文件之间的依赖关系存在不肯定性,没法保证顺序正确,所以出现了模块化。webpack
最小化的配置以下,主要是要配置source-map,和开发模式,这样打包出来的代码就是普通的代码,没有压缩、没有用eval包含,比较易读。而后就能够建立两个js文件各类尝试了。(本次使用的 webpack 版本为:4.43.0)es6
// webpack.config.js
{
// ...
mode: 'development',
devtool: 'source-map',
module: {
rules: [
{
test: /\.js$/,
exclude: /(node_modules)/,
loader: 'babel-loader',
}
]
}
// ...
}
// .babelrc
{
"presets": [
"@babel/preset-env"
]
}
复制代码
CommonJS规范的实现很是简单,放在前面来讲。web
CommonJS规范是Node.js处理模块的标准。npm生态就是创建在CommonJS标准之上的。npm
能够导出的有:变量、function、对象等。 如今使用频率最高。它使用同步加载的方式将模块一次性加载出来。浏览器
使用示例:缓存
/* 导出 */
// 直接挂到exports上
exports.uppercase = str => str.toUpperCase();
// 挂到module.exports上
module.exports.a = 1;
// 重写module.exports
module.exports = { xxx: xxx };
/* 导入 */
// 能够访问package.a / package.b...
const package = require('module-name');
// 结构赋值
const { a, b, c } = require('./uppercase.js');
复制代码
下面是两个具备引用关系的模块的打包文件,删去各类花里胡哨的注释后,露出了很是简单的真面目。服务器
源码:
// index.js
const m = require('./module1.js');
console.log(m)
// module1.js
const m = 1;
module.exports = m;
复制代码
打包后的文件以下:
能够很清楚地看到,webpack把整个打包后的代码处理成了一个当即执行的函数,参数是一个包含全部模块的对象,每一个文件表现为一个键值对,用文件路径字符串做为属性名,将文件内的代码,也就是整个模块的内容全都包装进了一个函数里做为属性的值。在模块内部使用的require也用__webpack_require__
方法来替换了。
// 1. 是一个当即执行的函数
(function (modules) {
var installedModules = {};
// 4. 执行函数
function __webpack_require__(moduleId) {
// 5. 检查若是有缓存直接返回
if (installedModules[moduleId]) {
return installedModules[moduleId].exports;
}
// 6. 建立一个模块并存入缓存
var module = installedModules[moduleId] = {
i: moduleId,
l: false,
exports: {}
};
// 7. 根据模块id从模块对象里取出并执行
// this绑定到module.exports 并注入module, module.exports
modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
// 8. 标记这个模块为已加载
module.l = true;
// 9. 返回module.exports
return module.exports;
}
// 3. 传入入口文件名
return __webpack_require__(__webpack_require__.s = "./index.js");
})({ // 2. 将模块对象做为参数传入
"./index.js":
(function (module, exports, __webpack_require__) {
var m = __webpack_require__("./module1.js")
console.log(m)
}),
"./module1.js":
(function (module, exports) {
var m = 1;
module.exports = m;
})
});
复制代码
顺着执行过程能够发现CommonJS模块处理的流程:
__webpack_require__
,给要执行的模块(文件)建立一个module。var module = {
i: moduleId,
exports: {}
}
复制代码
__webpack_require__
方法。modules[moduleId].call(module.exports, module, module.exports, __webpack_require__)
复制代码
因此咱们知道了,上面的使用示例中使用exports.xxx = xxx
和module.exports.xxx = xxx
,其实本质上是同样的,都是将变量挂到module.exports
对象中,甚至还能够写成this.exports.xxx = xxx
也有一样的效果;别的模块导入时,获得的也是module.exports
对象。
所以,若是直接使用module.exports = { xxx: xxx }
的方式,就至关于重写了导出的模块,故而对 exports
和 this.exports
的任何操做也就没什么意义了。
在ES6之前,JS没有模块体系。只有社区指定的一些模块加载方案,如用于服务器端的CommonJS和用于浏览器端的AMD。
ES6导出的不是对象,没法引用模块自己,模块的方法单独加载。所以能够在编译时加载(也即静态加载),于是能够进行静态分析,在编译时就能够肯定模块的依赖关系和输入输出的变量,提高了效率。
而CommonJS和AMD输出的是对象,引入时须要查找对象属性,所以只能在运行时肯定模块的依赖关系及输入输出变量(即运行时加载),所以没法在编译时作“静态优化”。
使用示例以下:
/* 导出 */
// 导出一个变量
export let firstName = 'Michael';
// 导出多个变量
let firstName = 'Michael';
let lastName = 'Jackson';
export { firstName, lastName };
// 导出一个函数
export function multiply(x, y) {
return x * y;
}
// 给导出的变量重命名
export {
v1 as streamV1,
v2 as streamV2
};
// 默认输出(本质上将输出变量赋值给default),import时能够随便命名且无需加大括号{}:
export default function crc32() {}
/* 引用 */
// import只能放在文件顶层
import { stat, exists, readFild } from 'fs';
import { lastName as surname } from './profile.js'
// 引入整个模块,而后用circle.xxx获取内部变量或方法
import * as circle from './circle';
import crc from 'crc32';
复制代码
源码:
// index.js
import { m } from './module1';
console.log(m);
// module1.js
const m = 1;
const n = 2;
export { m, n };
复制代码
打包后:
// 1. 是一个当即执行函数
(function (modules) {
var installedModules = {};
// 4. 处理入口文件模块
function __webpack_require__(moduleId) {
if (installedModules[moduleId]) {
return installedModules[moduleId].exports;
}
// 5. 建立一个模块
var module = installedModules[moduleId] = {
i: moduleId,
l: false,
exports: {}
};
// 6. 执行入口文件模块函数
modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
module.l = true;
// 7. 返回
return module.exports;
}
__webpack_require__.d = function (exports, name, getter) {
if (!__webpack_require__.o(exports, name)) { // 判断name是否是exports本身的属性
Object.defineProperty(exports, name, {enumerable: true, get: getter});
}
};
__webpack_require__.r = function (exports) {
if (typeof Symbol !== 'undefined' && Symbol.toStringTag) {
// Symbol.toStringTag做为对象的属性,值表示这个对象的自定义类型 [Object Module]
// 一般只做为Object.prototype.toString()的返回值
Object.defineProperty(exports, Symbol.toStringTag, {value: 'Module'});
}
Object.defineProperty(exports, '__esModule', {value: true});
};
__webpack_require__.o = function (object, property) {
return Object.prototype.hasOwnProperty.call(object, property);
};
// 3. 传入入口文件id
return __webpack_require__(__webpack_require__.s = "./index.js");
})({ // 2. 模块对象做为参数传入
"./index.js":
(function (module, __webpack_exports__, __webpack_require__) {
// __webpack_exports__就是module.exports
"use strict";
// 添加了__esModule和Symbol.toStringTag属性
__webpack_require__.r(__webpack_exports__);
var _module1__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__("./module1.js");
console.log(_module1__WEBPACK_IMPORTED_MODULE_0__["m"])
}),
"./module1.js":
(function (module, __webpack_exports__, __webpack_require__) {
"use strict";
__webpack_require__.r(__webpack_exports__);
// 把m/n这些变量添加到module.exports中,并设置getter为直接返回值
__webpack_require__.d(__webpack_exports__, "m", function () {return m;});
__webpack_require__.d(__webpack_exports__, "n", function () {return n;});
var m = 1;
var n = 2;
})
});
复制代码
能够看到,跟CommonJS差很少,一样也是将模块对象传入一个当即执行的函数。只是模块函数内部稍稍复杂了一些。执行一个模块的流程前两步和CommonJS同样,也是先建立了一个module,而后再绑定this到module,传入module和module.exports对象。
在模块内部,导出模块的流程是:
__webpack_exports__
也就是module.exports对象添加一个Symbol.toStringTag属性值为{value: 'Module'}
,这么作的做用就是使得module.exports调用toString方法能够返回[Object Module]
来代表这是一个模块。导入的表现也很不同,不是单单导入module.exports这个对象,而是作了一些额外的工做。在这个例子中,index.js文件引入了m,而后打印了m。可是打包结果倒是导入的m和访问的m并不相同,访问m的时候其实访问的是m['m'],也就是说webpack在访问的时候自动帮咱们访问内部的同名属性。
import { m } from './module1';
console.log(m);
// webpack
var _module1__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__("./module1.js");
console.log(_module1__WEBPACK_IMPORTED_MODULE_0__["m"])
复制代码
不一样的导入和导出方式也会有不一样的表现:
var a = 1; export a;
至关于 export 1
没意义,会报错。export { bar, foo }
export var bar = xxx
export function foo = xxx 复制代码
const obj = { a: 1 };
export { obj };
// webpack
// __webpack_exports__就是导出的结果
__webpack_require__.d(__webpack_exports__, "obj", function() { return obj; });
var obj = { a: 1 };
// __webpack_require__.d这个函数首先判断要导出的变量是否是__webpack_exports__上的属性
// 若是不是就把这个变量挂在__webpack_exports__上,并设置getter
__webpack_require__.d = function(exports, name, getter) {
if(!__webpack_require__.o(exports, name)) {
Object.defineProperty(exports, name, { enumerable: true, get: getter });
}
};
复制代码
__webpack_exports__.default
上let obj = { a: 1 }
export default { obj }
// webpack
var obj = { a: 1 };
__webpack_exports__["default"] = ({ obj: obj });
// export default后面跟什么值就没有限制了
export default obj
// webpack
__webpack_exports__["default"] = (obj);
export default obj.a
// webpack
__webpack_exports__["default"] = (obj.a);
export default 1
// webpack
__webpack_exports__["default"] = (1);
复制代码
__webpack_exports__
,访问的时候会自动查找default属性的值,若是导出没有使用export default,会获得一个undefined。import obj from './module1'
console.log(obj) // 没有default会获得undefined
obj.c = 2; // 没有default会报错
// webpack
var _module1__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__("./module1.js");
console.log(_module1__WEBPACK_IMPORTED_MODULE_0__["default"]);
_module1__WEBPACK_IMPORTED_MODULE_0__["default"].c = 2;
复制代码
*
总体导入,不会自动查找内层属性,直接访问__webpack_exports__
import * as obj from './module1'
console.log(obj)
obj.c = 2;
// webpack
var _module1__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__("./module1.js");
console.log(_module1__WEBPACK_IMPORTED_MODULE_0__);
_module1__WEBPACK_IMPORTED_MODULE_0__["c"] = 2;
复制代码
import { obj } from './module1'
console.log(obj)
obj.c = 2;
// webpack
var _module1__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__("./module1.js");
console.log(_module1__WEBPACK_IMPORTED_MODULE_0__["obj"]);
_module1__WEBPACK_IMPORTED_MODULE_0__["obj"].c = 2;
复制代码
如今就能明白文章开头所说的动态加载、静态加载、复制、引用都是什么意思了。
CommonJS导出的是对象,内部要导出的变量在导出的那一刻就已经赋值给对象的属性了,也就有了“CommonJS输出的是值的拷贝”这种说法,后面再在模块里修改变量,其余模块是感受不到的,由于已经没有关系了。可是对象仍是会影响,由于对象拷贝的只是对象的引用。
也是由于CommonJS导出的是对象,在编译阶段不会读取对象的内容,并不清楚对象内部都导出了哪些变量、这些变量是否是从别的文件导入进来的。只有等到代码运行时才能访问对象的属性,肯定依赖关系。所以才说CommonJS的模块是动态加载的。
而对ES6 Module来讲,因为内部对每一个变量都定义了getter,所以其余模块导入后访问变量时触发getter,返回模块里的同名变量,若是变量值发生变化,则外边的引用也会变化。
可是export default没有走getter的形式,也是直接赋值,因此输出的也是一份拷贝。例以下列代码,能够看到只是简单地将变量 m 的值拷贝了一份挂到 default 属性上。(经评论区提醒,webpack5 更正了这一行为,default 也走 getter 了。)
const m = 1;
export default m;
// webpack
(function (module, __webpack_exports__, __webpack_require__) {
"use strict";
__webpack_require__.r(__webpack_exports__);
var m = 1;
__webpack_exports__["default"] = (m);
})
复制代码
ES6 Module导出的不是一个对象,导出的是一个个接口,所以在编译时就能肯定模块之间的依赖关系,因此才说ES6 Module是静态加载的。Tree Shaking就是根据这个特性在编译阶段摇掉无用模块的。
ES6 Module还提供了一个import()方法动态加载模块,返回一个Promise。