CommonJS

CommonJS 模块规范

CommonJS 是 Node 的规范,是一直沿用至今的一个模块规范,。虽然 ES6 提出了新的模块规范,但目前为止 Node 无法直接兼容 ES6,我们按照 ES6 的模块规范来书写代码,但是实际上它们最终会被编译为 CommonJS 规范对应的代码来执行。

需要记住,Node 对获取的 JavaScript 文件内容进行了头尾包装。在头部添加了function (exports, require, module, __filename, __dirname) {\n,而在尾部添加了\n}。所以一个正常的 JavaScript 文件会被包装成如下的样子:

1
2
3
4
function (exports, require, module, __filename, __dirname) {
// .js文件的内容
// ......
}

模块文件 5 个变量的含义:

  • exports:一个对象,用来挂载当前模块中要导出的内容;
  • require:一个用来引入其他模块的函数;
  • module:一个代表模块自身的对象;
  • __filename:当前模块文件的绝对路径;
  • __dirname:当前模块文件所在目录的绝对路径。

注意千万不要重写这些变量,否则会导致它们与模块文件之间的联系被切断。

下面的例子是我创建了一个名为 test.js 的文件并用 node 运行得到的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// test.js文件
console.log(exports); // {}
console.log(typeof require); // function
console.log(__filename); // D:\Others\test.js
console.log(__dirname); // D:\Others
console.log(module); // Module {
// id: '.',
// exports: {},
// parent: null,
// filename: 'D:\\Others\\test.js',
// loaded: false,
// children: [],
// paths: [ 'D:\\Others\\node_modules', 'D:\\node_modules' ]
// }

现在我创建了两个文件 a.js 和 b.js,在 a.js 中演示exports的使用,在 b.js 中演示require的使用。

1
2
3
4
5
6
7
8
// a.js
let name = 'a.js';

let sayName = function () {
console.log(`my name is ${name}`);
};

exports.sayName = sayName;
1
2
3
4
// b.js
var moduleA = require('./a.js');

moduleA.sayName(); // my name is a.js

在 a.js 文件中,通过将函数sayName()挂载到exports对象上,来导出sayName()。而在 b.js 文件中,通过require()函数获取了 a.js 文件中要导出内容组成的对象。

我们已经知道了 Node 会将每个模块文件包装为一个函数,而exports对象实际上是通过形参的方式传入的,直接赋值形参会改变形参的引用。所以为了防止无意间重写exports,推荐使用module.exports来替代它。下面演示下初学者容易犯的错误,和推荐的用法:

1
2
3
4
5
6
7
8
9
10
11
12
// a1.js
let name = 'a1.js';

let sayName = function () {
console.log(`my name is ${name}`);
};

// 错误用法
exports = {
name,
sayName,
};
1
2
3
4
5
6
7
8
9
10
11
// a2.js
let name = 'a2.js';

let sayName = function () {
console.log(`my name is ${name}`);
};
// 推荐用法
module.exports = {
name,
sayName,
};
1
2
3
4
5
6
7
// b.js
var moduleA1 = require('./a1.js');
var moduleA2 = require('./a2.js');

moduleA1.sayName(); // TypeError: moduleA.sayName is not a function

moduleA2.sayName(); // my name is a2.js

模块加载机制

CommonJS 模块的重要特性是加载时执行,即模块文件内的代码会在被require()的时候,就会全部执行。当执行完成后,module.exports对象就代表着被加载的模块文件要导出的内容。

有两点需要注意:

  • var moduleA = require('./a.js')中的moduleA,是由 a.js 文件中的module.exports经过一次浅拷贝得到的;
  • 同一个模块文件,也许会被多次加载,但是只会执行一次。

浅拷贝的操作类似于clone函数:

1
2
3
4
5
6
7
8
9
function clone(obj) {
let newObj = {}

for (let key in ) {
newObj[key] = obj[key]
}

return newObj
}

在明白上面说述内容后,请观察一个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// a.js
let count = 0;
let obj = {
num: 0,
};

let show = function () {
console.log(`count is: ${count}`);
console.log(`obj.num is: ${obj.num}`);
};

let change = function () {
count++;
obj.num++;
};

module.exports = {
count,
obj,
show,
change,
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// b.js
var moduleA = require('./a.js');

console.log(moduleA.count); // 0
console.log(moduleA.obj.num); // 0
moduleA.show(); // count is: 0
// obj.num is: 0

moduleA.change();

console.log(moduleA.count); // 0
console.log(moduleA.obj.num); // 1
moduleA.show(); // count is: 1
// obj.num is: 1

通过change()函数同时改变了countobj.num的值。在 a.js 文件中,countobj.num的值均变为1,而在 b.js 文件中moduleA.count的值还是为 0 没有变化,moduleA.obj.num的值则变为1。如果你没看懂,你可能需要看这里

模块循环 require()

在 CommonJS 中如果出现模块“循环require()”,只会输出已经执行的部分,还未执行的部分不会输出。下面贴出官方文档里面的例子

1
2
3
4
5
6
7
8
9
10
11
// 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');
1
2
3
4
5
6
7
8
9
10
11
// b.js
console.log('b starting');

exports.done = false;

const a = require('./a.js');
console.log('in b, a.done = %j', a.done);

exports.done = true;

console.log('b done');
1
2
3
4
5
6
7
// main.js
console.log('main starting');

const a = require('./a.js');
const b = require('./b.js');

console.log('in main, a.done = %j, b.done = %j', a.done, b.done);
1
2
3
4
5
6
7
8
9
依次输出:
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

When main.js loads a.js, then a.js in turn loads b.js. At that point, b.js tries to load a.js. In order to prevent an infinite loop, an unfinished copy of the a.js exports object is returned to the b.js module. b.js then finishes loading, and its exports object is provided to the a.js module.



感谢您的阅读,如果发现文章中有错误或漏洞,请批评指正。
邮箱:aadonkeyz@gmail.com

0%