# import、require、export、module.export 总结

# 1. CommonJS规范

exportsmodule.exportsmodulerequire属于CommonJS规范

# 1.1 介绍

const { run, eat } = require('./dog.js');

CommonJS使用require引入模块的方式是动态的,所谓动态就是上面代码在被执行的时候,才会引入dog.js模块,而且引入的是完整的一个对象,并不只是runeat两个方法。所以上面这段代码也可以和下面的代码等同

const dog = require('./dog.js');
const { run } = dog;
const { eat } = dog;
1
2
3

# 1.2 module

CommonJS中,一个文件就是一个模块module就表示当前模块的引用(module是一个对象)。module作为一个对象自然也就有关于当前模块信息的属性。常见的有:module.exportsmodule.childrenmodule.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');
1
2
3
4
5
6
// 直接赋值给exports进行导出
module.exports = function (data) {
  console.log(data);
};
// 引入
const eat = require('./dog.js');
1
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;
1
2
3
4
5
6
7

# 1.5 require

require()用于引入模块、JSON、本地文件,这里只对引入模块做说明。其参数可以是模块名,也可以是文件路径。如果直接使用模块名,则会在node_modules中或者内置模块中进行引入。如果引入的是模块,该方法的返回值就是module.exports对象

# 2. ES6的模块规范介绍

importimport()exportexport default属于ES6的模块规范

ES6采用import的方式引入模块,这种方式和CommonJS正好相反,它是静态的引入模块,即在代码编译的时候就已经把runeat方法引入了。所以在效率上会比CommonJSrequire方法效率更高

# 2.1 export

注意和CommonJSexports进行区别,export在ES6中是个关键字,exports在CommonJS中是一个对象或属性。也就是说exports必须使用=对自身的属性进行赋值,而export则使用声明的方式导出变量。

// CommonJS
exports.eat = function () {};
1
2
// ES6

export function eat () {}
1
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 };
1
2
3
4
5
6
7
8
// 报错 export必须导出具有对应关系的变量
const eat = () => {}
export eat;
// 正确
export function eat1 () {}
export { eat2 };
1
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
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";
1
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
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
1
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);
1
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
1
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 规范
1
2
3

# 3. CommonJS和ES6的差异

类型 CommonJS ES6
引入 require importimport()
导出 exportsmodule.exports exportexport 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;
1
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
1
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;
1
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 }
1
2
3
4
5
6
7
// b.js
let count = 0;
setTimeout(() => {
  count += 1;
  console.log('base.count:', count);
}, 500);
export { count };
1
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
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, // 布尔值,表示该模块的脚本是否执行完毕
  ...
}
1
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
1
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);
}
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
1
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)

ES6 import/export 静态编译 (opens new window)

javascript模块循环加载 (opens new window)

Last Updated: 9/16/2025, 2:58:59 PM