# import、require、export、module.export 总结
# 1. CommonJS规范
exports、module.exports、module、require属于CommonJS规范
# 1.1 介绍
const { run, eat } = require('./dog.js');
CommonJS使用require引入模块的方式是动态的,所谓动态就是上面代码在被执行的时候,才会引入dog.js模块,而且引入的是完整的一个对象,并不只是run和eat两个方法。所以上面这段代码也可以和下面的代码等同
const dog = require('./dog.js');
const { run } = dog;
const { eat } = dog;
2
3
# 1.2 module
在CommonJS中,一个文件就是一个模块,module就表示当前模块的引用(module是一个对象)。module作为一个对象自然也就有关于当前模块信息的属性。常见的有:module.exports、module.children、module.parent等等,这里只需要关注module.exports就行。
# 1.3 module.exports
module.exports也是一个对象,该对象由系统创建,在外部文件引入此模块时实际就是引入了exports对象。一般我们都采用module.exports.xxx的方式导出数据,也可以使用直接给exports赋值的方式导出数据。
// 导出dog.js
module.exports.eat = function (data) {
console.log(data);
};
// 引入
const { eat } = require('./dog.js');
2
3
4
5
6
// 直接赋值给exports进行导出
module.exports = function (data) {
console.log(data);
};
// 引入
const eat = require('./dog.js');
2
3
4
5
6
# 1.4 exports
exports是个值得注意的地方。它的使用方法和module.exports是一样的,类似于module.exports的快捷方式。非常要注意的是不要直接给exports赋值,只能使用.对exports的属性进行赋值,如果使用=直接给exports赋值会导致数据不能导出。
// 正确使用 等同于module.exports.eat
exports.eat = function (data) {
console.log(data);
};
// 无法导出
exports = 123;
2
3
4
5
6
7
# 1.5 require
require()用于引入模块、JSON、本地文件,这里只对引入模块做说明。其参数可以是模块名,也可以是文件路径。如果直接使用模块名,则会在node_modules中或者内置模块中进行引入。如果引入的是模块,该方法的返回值就是module.exports对象
# 2. ES6的模块规范介绍
import、import()、export、export default属于ES6的模块规范
ES6采用import的方式引入模块,这种方式和CommonJS正好相反,它是静态的引入模块,即在代码编译的时候就已经把run和eat方法引入了。所以在效率上会比CommonJS的require方法效率更高
# 2.1 export
注意和CommonJS中exports进行区别,export在ES6中是个关键字,exports在CommonJS中是一个对象或属性。也就是说exports必须使用=对自身的属性进行赋值,而export则使用声明的方式导出变量。
// CommonJS
exports.eat = function () {};
2
// ES6
export function eat () {}
2
3
export用于暴露模块对外的接口。这里需要注意export暴露的是变量而不是值。注意这两者的区别,下面代码对变量和值进行了解释。(个人理解import取得是当前变量的引用,所以必须是变量而不是值)
// 报错,导出的是1,并非a,1是值,export不能直接导出值
const a = 1;
export a; // SyntaxError: Invalid or unexpected token
// 正确,导出的是b
export const b = 1;
// 正确,导出的是一个对象
const a1 = 1;
export { a1 };
2
3
4
5
6
7
8
// 报错 export必须导出具有对应关系的变量
const eat = () => {}
export eat;
// 正确
export function eat1 () {}
export { eat2 };
2
3
4
5
6
# 2.2 export default
export default用于直接导出值,比如直接导出数值、字符串、对象、数组、方法等。在使用import引入的时候,直接给导出的值一个变量就行了
// dog.js
export default 1;
// test.js
import dog from 'dog.js';
console.log(dog); // 1
2
3
4
5
6
# 2.3 import
import是输入接口,用来引入外部模块暴露出来的变量或者值,接口是只读的不可以被改变,如果接口是对象则可以更改对象的属性,但是不建议这样做。所有输入进来的东西我们不应该去更改它的原始值。
import defaultExport from "module-name";
import * as name from "module-name";
import { export0 } from "module-name";
import { export0 as alias } from "module-name";
import { export1, export2 } from "module-name";
import { export3, export4 as alias2 , [...] } from "module-name";
import defaultExport1, { export5 [ , [...] ] } from "module-name";
import defaultExport2, * as name from "module-name";
import "module-name";
2
3
4
5
6
7
8
9
# 2.4 import 变量提升
// a.js
console.log('a.js');
import { foo as foo1 } from './b.js';
console.log(foo1);
// b.js
console.log('b.js');
export const foo = 1;
// 运行 node -r esm a.js
// b.js a.js 1
2
3
4
5
6
7
8
9
10
预编译 a.js -> 发现关键词 import -> 预编译 b.js -> 执行 b.js -> 执行 a.js
// a.js
console.log('I am a.js...')
import b from './b.js';
console.log('a.js b.foo:', b.foo);
import c from './c.js';
// b.js
console.log('b.js')
let foo = 1;
export default { foo };
// c.js
console.log('I am c.js...')
import b1 from './b.js';
console.log('c.js b.foo:', b1.foo);
b1.foo = b1.foo - 1;
export default {};
// 运行 node -r esm a.js
// b.js
// I am c.js...
// c.js b.foo: 1
// I am a.js...
// a.js b.foo: 0
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// a.js
console.log('a.js');
const b = require('./b');
console.log(b.foo);
// b.js
console.log('b.js');
const foo = 1;
module.exports = { foo };
// 运行 node a.js
// a.js b.js 1
// 对 a.js 预编译时,只会把变量 b 的声明提前,a.js & b.js 预编译后的执行顺序如下
const b;
console.log('I am a.js...');
b = require('./b');
console.log(b.foo);
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// a.js
console.log('I am a.js...');
const b1 = require('./b');
console.log(b1.foo);
b1.foo = b1.foo - 1;
require('./c');
// b.js
console.log('b.js');
const foo = 1;
module.exports = { foo };
// c.js
console.log('I am c.js...');
const b2 = require('./b');
console.log(b2.foo);
// node a.js
// I am a.js...
// b.js
// 1
// I am c.js...
// 0
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import命令只能在模块顶层使用,不能在函数、判断语句等代码块之中引用;require可以。
# 2.5 node 中运行 es6
npm install esm
node -r esm xxx.js // xxx.js 中使用 ES6 模块规范
node xxx.js // xxx.js 中使用 CommonJS 规范
2
3
# 3. CommonJS和ES6的差异
| 类型 | CommonJS | ES6 |
|---|---|---|
| 引入 | require | import、import() |
| 导出 | exports、module.exports | export、export default |
| 加载模块 | 赋值过程,运行阶段去加载模块 | 解构过程,预编译阶段去加载模块 |
| 性能 | 性能较低,运行时引入模块,并且赋值 | 稍高 编译时引入制定模块 | |
| 加载方式 | 同步加载 | 异步加载 |
| 基础数据类型 | 复制该变量 | 只是对该变量的动态只读引用 |
| 复杂数据类型 | 浅拷贝该对象 | 只是对该变量的动态只读引用 |
目前所有的引擎都还没有实现import,我们在node中使用babel支持ES6,也仅仅是将ES6转码为ES5再执行,import语法会被转码为require
// b.js
let count = 0;
setTimeout(() => {
count += 1;
console.log('base.count:', count);
}, 500);
module.exports.count = count;
2
3
4
5
6
7
// a.js
const { count } = require('./b');
setTimeout(() => {
console.log('count in commonjs is', count);
}, 1000);
// node a.js
// base.count: 1
// count in commonjs is 0
2
3
4
5
6
7
8
// b.js
const count = { a: 1 };
setTimeout(() => {
count.b = 2;
count.a += 1;
console.log('base.count:', count);
}, 500);
module.exports.count = count;0;
2
3
4
5
6
7
8
// a.js
const { count } = require('./b');
setTimeout(() => {
console.log('count in es6 is', count);
}, 1000);
// node a.js base.count: { a: 2, b: 2 }
// count in es6 is { a: 2, b: 2 }
2
3
4
5
6
7
// b.js
let count = 0;
setTimeout(() => {
count += 1;
console.log('base.count:', count);
}, 500);
export { count };
2
3
4
5
6
7
// a.js
import { count } from './b';
setTimeout(() => {
console.log('count in es6 is', count);
}, 1000);
// base.count: 1
// count in es6 is 1
2
3
4
5
6
7
# 4. 循环引入
# 4.1 require加载原理
当Node遇到require(X)时,会按照下面顺序处理
- 如果X是内置模块,比如require('http'),返回该模块,不在继续执行
- 如果X以“./”、"/"、“../”开头,根据X所在的父模块,确定X的绝对路径,随后将X当成文件一次查找x、x.js、x.json、x.node,只要存在其中一个,就返回该文件
- 如果X不戴路径,根据X所在的父模块,确定X可能的安装目录,在每个目录中,将X当成文件名或者目录加载
- 抛出“not found”
CommonJS的一个模块就是一个脚本文件,require命令第一次加载该脚本,就会执行整个脚本,随后在内存中生成一个对象
{
id: '...', // 模块名
exports: { '...' }, // 模块输出的各个接口
loaded: true, // 布尔值,表示该模块的脚本是否执行完毕
...
}
2
3
4
5
6
以后需要用到这个模块的时候,就会到exports属性上面取值。即使再次执行require命令,也不会再次执行该模块,而是到缓存之中取值
// a.js
console.log('a starting');
exports.done = false;
const b = require('./b.js');
console.log('in a, b.done = %j', b.done);
exports.done = true;
console.log('a done');
// b.js
console.log('b starting');
exports.done = false;
const a1 = require('./a.js');
console.log('in b, a.done = %j', a1.done);
exports.done = true;
console.log('b done');
// main.js
console.log('main starting');
const a2 = require('./a.js');
const b2 = require('./b.js');
console.log('in main, a.done = %j, b.done = %j', a2.done, b2.done);
// node a.js
// main starting
// a starting
// b starting
// in b, a.done = false
// b done
// in a, b.done = true
// a done
// in main, a.done = true, b.done = true
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
# 4.2 ES6循环加载
ES6模块是动态引用,不存在缓存值的问题,它遇到模块加载命令import时,不会去执行模块,而是只生成一个引用。等到真的需要用到时,再到模块里面去取值。
// a.js
import { b } from './b';
let counter = 0;
export function a (n) {
counter += 1;
console.log(counter);
return n === 0 || b(n - 1);
}
2
3
4
5
6
7
8
9
// b.js
import { a } from './a.js';
export function b (n) {
return n !== 0 && a(n - 1);
}
// main.js
import * as m from './a.js';
const x = m.a(5);
console.log(x);
const y = m.a(4);
console.log(y);
// node main.js
// 1 2 3 false 4 5 6 true
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
可以看出counter的值是累加的,ES6是动态引用。
# 5. 参考资料
理解import、export、module.exports、require等 (opens new window)