将程序划分成一个个小的结构,在每个结构中编写属于自己的逻辑代码。每个结构有自己独立的作用域,定义变量名不会影响到其他的结构;可以暴露变量、函数、对象等给其他结构使用;也可以导入其他结构的变量、函数、对象等。这种结构就是模块。
按照这种结构划分程序进行开发的过程,就是模块化开发。
之前的 JavaScript 没有模块化:
之前的 JavaScript 没有模块化。
例如:在 index1.js
中定义一个变量 name 为 Lee,在 index2.js
中定义一个变量 name 为 Mary,将它们都引入到 index.html
文件中,变量名就会互相影响。因为只是将 JavaScript 代码分开到了不同的 JavaScript 文件中,但这些 JavaScript 文件是没有独立的作用域的,它们其实还是都在全局作用域下。
// index1.js
var name = 'Lee'
// index2.js
var name = 'Mary'
// index.html
<script src="../src/js/index1.js"></script>
<script src="../src/js/index2.js"></script>
console.log(name) // Mary。变量名相互影响,因为 index2.js 在 index1.js 后面引入,因此打印 Mary
之前是使用立即执行函数来解决这个问题的。
// index1.js
// 强制给 JavaScript 文件中的变量增加了一个函数作用域
var module1 = (function(){
var name = 'Lee'
// 将变量返回出去
return {
name
}
})()
// index2.js
// 强制给 JavaScript 文件中的变量增加了一个函数作用域
var module1 = (function(){
var name = 'Mary'
// 将变量返回出去
return {
name
}
})()
// index.html
<script src="../src/js/index1.js"></script>
<script src="../src/js/index2.js"></script>
// 变量名不会相互影响
console.log(module1.name) // Lee
console.log(module2.name) // Mary
但这种解决方案,也有一定的问题:在没有合适的规范的情况下,可能会对模块任意命名,甚至出现模块名称相同的情况;而且必须得记住每一个模块名,才能在使用的时候正确使用;代码写起来混乱不看,每个文件中的代码都需要包裹在一个匿名函数中来编写。
AMD:
AMD 只是一种规范,它采用异步方式加载模块,模块的加载不影响它后面语句的运行。AMD 规范应用在浏览器端。
实现了 AMD 规范的比较常用的库有:require.js
和 curl.js
。
CommonJS:
CommonJS 只是一种规范,主要应用在服务器端。
Node 是 CommonJS 在服务器端的一个具有代表性的实现,因此可以直接在 Node 中运行 CommonJS;Browserify (一个浏览器端代码模块化的工具)是 CommonJS 在浏览器中的一个实现,浏览器自身并不支持 CommonJS,因此不能直接在浏览器上运行 CommonJS。
CommonJS 的缺点:CommonJS 加载模块是同步的,这意味着只有等到引入的模块加载完毕,当前模块中的内容才能运行。这在服务器端不会有什么问题,因为服务器加载的 JS 文件都是本地文件,加载速度非常快;但如果是在浏览器端,将会导致很长时间的阻塞。
导出模块:
导出模块使用 exports 和 module.exports
。
- exports:是一个对象,在这个对象上添加的属性会被导出。这种方式在开发中很少使用。
// utils.js const name = 'Lee' exports.name = name
module.exports
:module 对象有一个 exports 属性,指向的是一个对象,在这个对象上添加的属性会被导出。Node 导出的本质就是在导出module.exports
对象。CommonJS 规范中是没有
module.exports
的,只有 exports。
在 Node 中使用的是 Module 的类,每一个模块都是 Module 类的一个实例 module,因此在 Node 中真正用于导出的其实是module.exports
。
为了兼容 CommonJS 规范,Node 中才保留了 exports 这个关键字,让module.exports
默认指向 exports。// 可以简单地理解为,CommonJS 在每个模块的头部默认添加了以下代码: var module = { exports:{} } var exports = module.exports;
// Node 导出的本质就是在导出 module.exports 对象。module.exports 和 exports 默认指向的对象是同一个对象,因此 exports 导出才有效 // utils.js exports.name = 'Lee' module.exports.name = 'Mary' // main.js const utils = require('./utils.js') console.log(utils.name) // Mary // utils.js module.exports.name = 'Mary' exports.name = 'Lee' // main.js const utils = require('./utils.js') console.log(utils.name) // Lee
// Node 导出的本质就是在导出 module.exports 对象。此时,module.exports 指向的是一个新的对象,和 exports 指向的对象不再是同一个对象,此时 export 导出无效。这种方式在开发中最常用 // utils.js module.exports = { name: 'Mary', } exports.name = 'Lee' // main.js const utils = require('./utils.js') console.log(utils.name) // Mary
导入模块:
导入模块使用 require()
函数,可以引入一个模块中导出的对象。
// utils.js
const name = 'Lee'
module.exports.name = name
// main.js
const utils = require('./utils.js')
console.log(utils.name)
在 Node 中,每个 JavaScript 文件就是一个模块,并且 Node 已经实现了 CommonJS,因此在命令行中输入 node main.js
即可看到打印结果。
导入模块时查找文件的规则:
导入模块时,require(X)
查找文件的常见规则:
- 如果 X 是 Node 内置模块(例如:path、http 等),那么停止查找,直接返回核心模块。
- 如果 X 以
./
或者../
或者/
等开头:- 首先会将 X 当做一个文件在对应的目录下查找。
- 如果有后缀名:直接按照后缀名的格式查找对应的文件。
- 如果没有后缀名,按照以下顺序查找:直接查找文件 X;查找
x.js
文件;查找X.json
文件;查找x.node
文件。
- 如果没有找到对应的文件,会将 X 当做一个文件夹。
- 查找目录下的 index 文件:查找
X/index.js
文件;查找X/index.json
文件;查找X/index.node
文件。 - 如果还没有找到,报错。
- 查找目录下的 index 文件:查找
- 首先会将 X 当做一个文件在对应的目录下查找。
- 如果 X 不以
./
或者../
或者/
等开头,直接就是一个非 Node 内置模块的名称 X:- 首先会在当前目录的
node_modules
中,查找 X 文件夹下的index.js
。 - 如果没有找到,会去上层目录的
node_modules
中查找,以此类推,直到找到根目录。
- 首先会在当前目录的
导入模块的加载过程:
-
模块在第一次被导入时,其中的 JavaScript 代码会被从上到下执行一次。
-
模块被多次导入时,会缓存,其中的 JavaScript 也只会执行一次。
原理是:每个模块模块对象 module 都有有一个 loaded 属性,默认为 false,只要被导入执行过一次,就变为 true,如果再次导入,发现 loaded 为 true,就不会再执行了。
// utils.js console.log('utils') // main.js require('./utils.js') console.log('main') require('./utils.js') // 执行 node main.js,会打印 utils main
-
如果出现循环引入,Node 采用的是深度优先算法。
如上图,加载顺序是:main --> aaa --> bbb --> ccc --> ddd --> eee --> bbb
。
CommonJS 在 Node 中实现的导出和导入的本质:
CommonJS 在 Node 中实现的导出和导入的本质其实就是引用赋值,通过 require()
找到 nodule.exports
对象,然后将其赋值给变量。
// utils.js
const name = 'Lee'
exports.name = name
setTimeout(() => {
exports.name = 'Mary'
}, 1000)
// main.js
const utils = require('./utils.js')
console.log(utils.name) // 最开始打印 Lee
setTimeout(() => {
console.log(utils.name) // 两秒钟后打印 Mary。因为 utils 和 exports 指向的是同一个对象
}, 2000)
CMD 规范:
CMD 只是一种规范,也是采用异步方式加载模块,但是它将 CommonJS 的优点也吸收了过来。CMD 规范是应用于浏览器端的。
实现了 AMD 规范的比较常用的库有:sea.js
。
ES6 中的 ES Module:
直到 ES6,ECMAScript 官方才推出了 JavaScript 的模块化方案 ES Module。浏览器自身就支持 ES Module。
在此之前,为了让 JavaScript 支持模块化,社区涌现出了很多不同的模块化规范:AMD、CommonJS、CMD 等。但是目前,AMD 和 CMD 已经很少使用了。
Webpack 支持对 CommonJS 和 ES Module 的转化。开发中使用 CommonJS 和 ES Module 进行开发,最后经由 Webpack 打包后就只是普通的没有使用模块化的 JS 文件,就样的话,即使浏览器不支持 CommonJS 和 ES Module ,也可以在上面运行了。