一篇不是标题党的CommonJS和ES6模块规范讲解|博客系列
# 原始写法
在没有CommonJS和ES6的时候,我们想要达到模块化的效果可能有这么三种:
# 1. 一个函数就是一个模块
<script>
function m1 () {
// ...
}
function m2 () {
// ...
}
</script>
缺点:污染了全局变量,无法保证不会与其它模块发生冲突,而且模块成员之间看不出直接关系。
# 2. 一个对象就是一个模块
对象写法 为了解决上面的缺点,可以把模块写成一个对象,所有的模块成员都放到这个对象里面。
index.html
<script>
var module1 = new Object({
_sum: 0,
foo1: function () {},
foo2: function () {}
})
</script>
缺点:会暴露所有模块成员,内部的状态可能被改写。
例如,我们如果只是想暴露出两个方法而不暴露出_sum,就做不到。
而此时,_sum可能被外部改写:
module1._sum = 2;
# 3. 立即执行函数为一个模块
<script>
var module1 = (function() {
var _sum = 0;
var foo1 = function () {};
var foo2 = function () {};
return {
foo1: foo1,
foo2: foo2
}
})();
</script>
利用立即执行函数内的作用域已经闭包来实现模块功能,导出我们想要导出的成员。
此时外部代码就不能读取到_sum了:
console.log(module1._sum) // undefined
# 1. 暴露(定义)模块
正确的暴露方式:
暴露模块有两种方式:
module.exports = {}exports.xxx = 'xxx'
例如有一个m1.js文件:
第一种暴露方式:
module.exports = {
name: 'lindaidai',
sex: 'boy'
}
第二种暴露方式:
exports.name = 'lindaidai';
exports.sex = 'boy'
为什么可以有这两种写法呢?
我是这样理解的:module这个变量它代表的就是整个模块,也就是m1.js。而其实这个module变量是有一个属性exports的,它是一个叫做exports变量的引用,我们可以写一下伪代码:
var exports = {};
var module = {
exports: exports
}
return module.exports
(当然这只是伪代码啊,实际你这么去用会发现没有效果)
最后导出的是module.exports,而不是exports。
容易混淆的暴露方式:
如果你在代码中试图exports = { name: 'lindaidai' },你会发现在引入的地方根本获取不到name属性。
// m1.js
exports = {
name: 'lindaidai'
}
// test.js
const math = require('./m1.js')
console.log(m1); // {}
在控制台执行node test.js,发现打印出来的m1是一个空的对象。
我是这样理解的:整个模块的导出是靠module.exports的,如果你重新对整个exports对象赋值的话,它和module.exports就不是同一个对象了,因为它们指向的引用地址都不同:
module.exports -> {} // 指向一个空的对象
exports -> { name: 'lindaidai' } // 指向的是另一个对象
所以你对exports = {}做任何操作都影响不到module.exports。
让我们来看几个正确和错误的示例吧:
// m1.js
// 1. 正确
module.exports = {
name: 'lindaidai',
sex: 'boy'
}
// 2. 正确
exports.name = 'lindaidai';
exports.sex = 'boy'
// 3. 正确
module.exports.name = 'lindaidai';
module.exports.sex = 'boy'
// 4. 无效
exports = {
name: 'lindaidai',
sex: 'boy'
}
可以看到
exports.name = xxx是module.exports.name = xxx的缩写。exports = {}确不是module.exports = {}的缩写。
# 2. 引用(引入)模块
对于模块的引用使用全局方法require()就可以了。
注意⚠️这个全局方法是node中的方法哈,它不是window下面的,所以如果你没做任何处理想直接在html里用肯定就是不行的了:
index.html:
<body>
<script>
var m1 = require('./m1.js')
console.log(m1);
</script>
</body>
例如上面👆这样你打开页面控制台肯定就报错了:
Uncaught ReferenceError: require is not defined
at index.html:11
而如果你是在另一个js文件中引用(例如test.js),并在终端执行node test.js是可以用的:
test.js:
var m1 = require('./m1.js')
console.log(m1);
那是因为你的电脑上全局安装了Node.js,所以可以这样玩。
所以我们可以发现require()它是Node.js中的一个全局方法,并不是CommonJS独有的,CommonJS只是众多规范中的其中一种。
这种规范允许我们:
- 使用
module.exports = {}或者exports.name = xxx导出模块 - 使用
const m1 = require('./m1')引入模块
注意⚠️:
另外还有一点比较重要,那就是require()的参数甚至能允许你是一个表达式。
也就是说你可以把它设置为一个变量:
test.js:
var m1Url = './m1.js';
var m1 = require(m1Url);
// 甚至做一些字符串拼接:
var m1 = require('./m' + '1.js');
但是需要注意咯,这个传参可以为表达式并不是require特有的。因为JS语言是传值调用,函数或者方法在调用的时候参数会被先计算出来,因此在我们使用require方法并传入表达式的时候,会先计算出表达式的值再传递给require。(感谢掘友骑自行车的指出)
# 3. 模块标识符(标识)
模块标识符其实就是你在引入模块时调用require()函数的参数。
你会看到我们经常会有这样的用法:
// 直接导入
const path = require('path');
// 相对路径
const m1 = require('./m1.js');
// 直接导入
const lodash = require('lodash');
这其实是因为我们引入的模块会有不同的分类,像path这种它是Node.js就自带的模块,m1是路径模块,lodash是我们使用npm i lodash下载到node_modules里的模块。
分为以下三种:
- 核心模块(
Node.js自带的模块) - 路径模块(相对或绝对定位开始的模块)
- 自定义模块(
node_modules里的模块)
三种模块的查找方式:
- 核心模块,直接跳过路径分析和文件定位
- 路径模块,直接得出相对路径就好了
- 自定义模块,先在当前目录的
node_modules里找这个模块,如果没有,它会往上一级目录查找,查找上一级的node_modules,依次往上,直到根目录下都没有, 就抛出错误。
自定义模块的查找过程:
这个过程其实也叫做路径分析。
现在我把刚刚的test.js来改一下:
// var m1 = require('./m1.js');
// console.log(m1);
console.log(module.paths)
然后在终端执行:
node test.js
会发现输出了下面的一个数组:
LinDaiDaideMBP:commonJS lindaidai$ node test.js
[
'/Users/lindaidai/codes/test/CommonJS和ES6/commonJS/node_modules',
'/Users/lindaidai/codes/test/CommonJS和ES6/node_modules',
'/Users/lindaidai/codes/test/node_modules',
'/Users/lindaidai/codes/node_modules',
'/Users/lindaidai/node_modules',
'/Users/node_modules',
'/node_modules'
]
这里所说的查找,是指查找你现在用的这个模块,我现在用的是test.js,你可能看不出什么效果。现在让我们来模拟一个我们使用npm i安装的一个自定义模块功能。
首先,我在根目录下新建了一个名叫node_modules的文件夹,并在其中新建了一个名叫lindaidai.js的文件,用来模拟一个npm安装的依赖。
目录结构:

稍微编写一下lindaidai.js:
module.exports = {
print: function () {
console.log('lindaidai')
}
}
console.log('lindaidai模块:', module.paths)
然后在test.js中引入这个lindaidai模块:
// var m1 = require('./m1.js');
// console.log(m1);
// console.log(module.paths)
var lindaidai = require('lindaidai');
lindaidai.print();
现在执行node test.js,会发现输出了:
