首先说明一下 CommonJS模块规范(node用的就是这个)是运行时加载,es6模块规范是编译时加载
- 运行时加载 ---- 加载的时候引入对应的模块,整体引入,即使你只有几个函数,但也是整体模块都引入进来了
- 编译时加载 ---- 编译的时候就知道需要引入模块中的那些函数,按需引入,在加载之前就已经确定好引入关系
- node------
require
引入 ,module.exports/exports
导出 - es6--------
import
引入,export/export default
导出
CommonJS模块规范
每个文件就是一个模块,有自己的作用域。在一个文件里面定义的变量、函数、类,都是私有的,对其他文件不可见。
CommonJS规范规定,每个模块内部,module
变量代表当前模块。这个变量是一个对象,它的exports
属性(即module.exports
)是对外的接口。加载某个模块,其实是加载该模块的module.exports
属性。
var x = 5;
var addX = function (value) {
return value + x;
};
module.exports.x = x;
module.exports.addX = addX;
var example = require('./example.js');
console.log(example.x); // 5
console.log(example.addX(1)); // 6
exports 与 module.exports
为了方便,Node为每个模块提供一个exports变量,指向module.exports。这等同在每个模块头部,有一行这样的命令。
var exports = module.exports;
指向的是同一个内存地址,但最终输出的是module.exports!!!
于是我们可以直接在 exports 对象上添加方法,表示对外输出的接口,如同在module.exports上添加一样。
export.x = function(){
console.log(1);
}
但是要注意!因为node
模块是通过module.exports
导出的,如果直接将exports
变量指向一个值,就切断了exports
与module.exports
的联系,导致意外发生:
// a.js
exports = function a() {};
// b.js
const a = require('./a.js') // a 是一个空对象
//utils.js
let a = 100;
console.log(module.exports); //能打印出结果为:{}
console.log(exports); //能打印出结果为:{}
exports.a = 200; //这里辛苦劳作帮 module.exports 的内容给改成 {a : 200}
exports = '指向其他内存区'; //这里把exports的指向指走
//test.js
var a = require('/utils');
console.log(a) // 打印为 {a : 200}复制代码
从上面可以看出,其实require导出的内容是module.exports的指向的内存块内容,并不是exports的。
简而言之,区分他们之间的区别就是 exports 只是 module.exports的引用,辅助后者添加内容用的。
本质上就是:
var exports = module.exports;
exports.x === module.exports.x
exports = {}
这时候就是重新定义了exports 原先是exports = module.exports
对外接口是module.exports 所以重新赋值exports就会有问题
正确的用法是
exports.x = function(){}
module.exports.y = {}
exports.x = function(){}
module.exports = {
....
}
ES6模块规范
不同于CommonJS,ES6使用 export 和 import 来导出、导入模块。es6模块规范是在编译时加载的
import
命令具有提升效果,会提升到整个模块的头部,首先执行- 由于
import
是编译时加载,所以不能使用表达式和变量 - import命令输入的变量都是只读的,因为它的本质是输入接口。也就是说,不允许在加载模块的脚本里面,改写接口,如果是一个对象,改写对象的属性是允许的。
foo();
import { foo } from 'my_module';
上面的代码不会报错,因为import
的执行早于foo
的调用。这种行为的本质是,import
命令是编译阶段执行的,在代码运行之前。
// 报错
import { 'f' + 'oo' } from 'my_module';
// 报错
let module = 'my_module';
import { foo } from module;
// 报错
if (x === 1) {
import { foo } from 'module1';
} else {
import { foo } from 'module2';
}
上面三种写法都会报错,因为它们用到了表达式、变量和if结构。在静态分析阶段,这些语法都是没法得到值的
export和export default
首先我们讲这两个导出,下面我们讲讲它们的区别
- export与export default均可用于导出常量、函数、文件、模块等
- 在一个文件或模块中,export、import可以有多个,export default仅有一个
- 通过export方式导出,在导入时要加{ },export default则不需要
- export能直接导出变量表达式,export default不行。
export
export var firstName = 'Michael';
var firstName = 'Michael';
export {firstName};
export function multiply(x, y) {
return x * y;
};
需要特别注意的是,export命令规定的是对外的接口,必须与模块内部的变量建立一一对应关系。
// 报错
export 1;
// 报错
var m = 1;
export m;
上面两种写法都会报错,因为没有提供对外的接口。第一种写法直接输出 1,第二种写法通过变量m,还是直接输出 1。1只是一个值,不是接口。正确的写法是下面这样。
// 写法一
export var m = 1;
// 写法二
var m = 1;
export {m};
// 写法三
var n = 1;
export {n as m};
上面三种写法都是正确的,规定了对外的接口m
。其他脚本可以通过这个接口,取到值1。它们的实质是,在接口名与模块内部变量之间,建立了一一对应的关系。
同样的,function
和class
的输出,也必须遵守这样的写法。
// 报错
function f() {}
export f;
// 正确
export function f() {};
// 正确
function f() {}
export {f};
export default
为了给用户提供方便,让他们不用阅读文档就能加载模块,就要用到export default
命令,为模块指定默认输出。
// export-default.js
export default function () {
console.log('foo');
}
上面代码是一个模块文件export-default.js
,它的默认输出是一个函数。
其他模块加载该模块时,import
命令可以为该匿名函数指定任意名字。
// import-default.js
import customName from './export-default';
customName(); // 'foo'
上面代码的import命令,可以用任意名称指向export-default.js输出的方法,这时就不需要知道原模块输出的函数名。需要注意的是,这时import命令后面,不使用大括号。