认识 CommonJS 模块化规范

591 阅读6分钟

什么是 CommonJS

CommonJS 是由社区提出的一种 JavaScript 的模块化方案,并由 Node.js 借鉴与实现。主要特点是通过 require()module.exports 实现模块的导入和导出。

模块定义

CommonJS 模块化方案中规定一个文件就是一个模块。每个模块都有属于自己的作用域,模块中定义的所有变量、函数都只在模块内部可访问,对外是不可见的,除非显式地导出。

模块自身的作用域是怎样实现的?其实在执行模块的代码之前,模块的代码会被封装在如下这样的函数中:

(function (exports, require, module, __filename, __dirname) {
  // Module code actually lives in here
});

通过将模块的代码封装在函数中,从而让模块有自己的作用域,同时这样做还可以向模块代码注入 moduleexports 对象,实现者可以用来从模块中导出值,并且还可以注入 __filename__dirname 变量,可以利用这些变量获取当前模块的 绝对文件名目录路径

模块导出

在 CommonJS 模块内部,可以通过 module.exports 导出变量、函数等,导出的内容可以是任何 JavaScript 数据类型。

function add(a, b) {
  return a + b;
}

function subtract(a, b) {
  return a - b;
}

module.exports = {
  add: add,
  subtract: subtract,
};

当然啦,用 exports 也能导出,不过 exports 本质上是 module.exports 的引用,exportsmodule.exports 的简写形式,相当于是一个语法糖。

下面的例子,跟上面使用 module.exports 导出的例子效果是一样的。

exports.add = function (a, b) {
  return a + b;
};

exports.subtract = function (a, b) {
  return a - b;
};

exports 与其他任何变量一样,如果将新值重新分配给 exportsexports 将失去与 module.exports 的引用,使得 exports 无法正常导出值。

// hello.js

exports = { hello: false };
// main.js

var main = require("./hello.js");
console.log("main ", main);

在终端运行 main 模块,可以发现 exports 重新分配新值后,便无法正常导出值了。

37.png

module.exportsexports 本质上引用的是同一个对象,看下面这个例子:

// hello.js

exports.a = 3;
module.exports.b = 4;
// main.js

var hello = require("./hello");
console.log(hello);

38.png

module.exports 被重新分配新的对象时,module.exports 会导出该对象,而由于 module.exports 的引用被修改,exports 导出会失效:

// hello.js

exports.a = 3;

module.exports = { b: 4 };

setTimeout(() => {
  console.log("exports  ", exports);
}, 1500);
// main.js

var hello = require("./hello");
console.log(hello);

在终端运行 main 模块,发现只导出了 module.exports 上的对象,变量 a 没有被导出

39.png

因此在日常开发中,使用 module.exports 导出是比 exports 好的,因为 exports 的导出有被 module.exports 导出“覆盖”的风险。

模块导入

在 CommonJS 模块化方案中,可以使用 require() 导入模块。模块是采用同步的方式在运行时加载。这也意味着,在加载模块的时候,会阻塞后续代码的执行。这也是 CommonJS 模块化方案不适合于浏览器端,适合在服务器端的原因。因为在服务器端编程中,模块文件存在本地硬盘中,读取速度非常快,受同步加载的机制影响较小。

// a.js

module.exports.name = "小明";

使用 require() 导入 a 模块

// main.js

var name = require("./a.js");
console.log(name);

CommonJS 模块导出的是值的拷贝(浅拷贝),本质是在加载 CommonJS 模块时,会执行 CommonJS 模块代码,然后在内存里产生缓存,当再次引用该 CommonJS 模块时,会直接从缓存中获取,这会导致拿到的模块不是最新的,即当被引用的模块内部改变自身导出的值时,在引用他的模块中得不到最新的值。如下面的例子:

模块 c1 导出了变量 foo ,并且值为 bar 。500 毫秒后,将变量 foo 的值设置为 baz ,然后打印 foo 变量的值。

// c1.js

var foo = "bar";
setTimeout(() => (foo = "baz"), 500);
module.exports.foo = foo;
setTimeout(() => {
  console.log("c1 == ", foo);
}, 500);

c2 模块中使用 require 加载 c1 模块,然后输出 foo 变量,并在 1500 毫秒后重新输出 foo

// c2.js

var { foo } = require("./c1.js");
console.log(foo);
setTimeout(() => console.log(foo), 1500);

在终端运行 c2 模块,发现虽然在 c1 内部 foo 变量的值已经变为 baz 了,但是外部引入他的模块得到的还是旧值 bar

35.png

如果要得到 CommonJS 模块内最新的值,可以通过 导出函数,在该函数返回模块内部的值,然后调用该函数获取模块内部最新的值。如下面的例子:

在 c1 模块中定义值为 barfoo 变量,并导出 getFoo 函数,该函数用于返回 foo 变量

// c1.js

var foo = "bar";
setTimeout(() => (foo = "baz"), 500);
// 导出函数
module.exports.getFoo = function () {
  return foo;
};

setTimeout(() => {
  console.log("c1 == ", foo);
}, 500);

c2 模块导入 getFoo 函数,并调用 getFoo 函数,输出 getFoo 函数的返回值。为了验证 getFoo 函数能否得到模块内部最新的值,延迟 1500 毫秒后再次输出调用 getFoo 函数得到的返回值。

// c2.js

var { getFoo } = require("./c1.js");
console.log(getFoo());
setTimeout(() => {
  console.log(getFoo());
}, 1500);

在终端运行 c2 模块,可以发现导出函数,通过该函数能获得模块内最新的值。

36.png

总结

CommonJS 是由社区提出的一种 JavaScript 的模块化方案,并由 Node.js 借鉴与实现。主要特点是通过 require()module.exports 实现模块的导入和导出。

在 CommonJS 模块化规范中,一个文件就是一个模块,每个模块都有自己的作用域,除非显式的导出,不然外部无法直接获取模块内部的变量。CommonJS 模块能产生自身作用域的原因是模块代码会被包装在匿名函数中,并且 JS 引擎会向匿名函数注入 __filename__dirname 等变量以方便开发者。

可以使用 module.exportsexports 导出模块的内容供外部使用,exports 只是 module.exports 的简写形式,module.exportsexports 本质上引用的是同一个对象,当直接为 module.exports 赋值时,exports的引用被修改,导致 exports 的导出会失效,因此在日常开发中,使用 module.exports 导出是比 exports 好的,因为 exports 的导出有被 module.exports 导出“覆盖”的风险。这也说明在同一个模块中,不建议 module.exportsexports 混合使用。

导入的 CommonJS 模块以同步的方式在运行时加载 。这也意味着,在加载模块的时候,会阻塞后续代码的执行。这也是 CommonJS 模块化方案不适合于浏览器端,适合在服务器端的原因。因为在服务器端编程中,模块文件存在本地硬盘中,读取速度非常快,受同步加载的机制影响较小。

CommonJS 模块导出的是一个值的拷贝(浅拷贝),本质是在加载 CommonJS 模块时,会执行 CommonJS 模块代码,然后在内存里产生缓存,当再次引用该 CommonJS 模块时,会直接从缓存中获取,这会导致拿到的模块不是最新的,即当被引用的模块内部改变自身导出的值时,在引用他的模块中得不到最新的值。