前言: 前端模块化一直被我们所提及,无论是前端框架应用还是Node.js,都离不开模块化,而现在最常用的就是CommonJS和ES6规范。
CommonJS
(1)CommonJS规范是一种同步加载模块的方式,也就是说,只有当模块加载完成后,才能执行后面的操作。由于Nodejs主要用于服务器端编程,而模块文件一般都已经存在于本地硬盘,加载起来比较快,
因此同步加载模块的CommonJS规范就比较适用。
(2)CommonJS规范规定,每一个JS文件就是一个模块,有自己的作用域;在一个模块中定义的变量、函数等都是私有变量,对其他文件不可见。
// number.js
let num = 1
function add(x) {
return num + x
}
module.exports.num = num
module.exports.add = add
在上面的number.js中,变量num和函数add就是当前文件私有的,其他文件不能访问。
同时CommonJS规定了,每个模块内部都有一个module变量,代表当前模块;这个变量是一个对象,它的exports属性(即module.exports)提供对外导出模块的接口。
module
module代表当前模块,我们打印下module是什么
id:模块的识别符,通常是带有绝对路径的模块文件名
filename:模块的文件名,带有绝对路径。
loaded:返回一个布尔值,表示模块是否已经完成加载。
parent:返回一个对象,表示调用该模块的模块。
children:回一个数组,表示该模块要用到的其他模块。
exports:模块对外输出的对象。
path:模块的目录名称。
paths:模块的搜索路径。
exports
exports和module.exports都是用于导出模块,他们指向的是一个地址,相当于var exports = module.exports,我们在导出的时候可以用诸如 exports.a = ‘1233’ 的形式来导出。
但是有一点需要注意的是不可以用exports = 'a’的形式导出,这种方式会切断exports 和 module.exports之间的联系,module.exports实际上还是指向了默认值(空对象),最终导出的结果也是空对象。
require
require是导入命令,它的基本功能是读取并执行js文件,并返回该模块导出的module.exports对象
以下是打印的require信息。
{
id: '.',
path: 'd:\\Users\\72150591\\Desktop\\新建文件夹\\beesapp',
exports: {},
parent: null,
filename: 'd:\\Users\\72150591\\Desktop\\新建文件夹\\beesapp\\b.js',
loaded: false,
children: [ [Module] ],
paths: [
'd:\\Users\\72150591\\Desktop\\新建文件夹\\beesapp\\node_modules',
'd:\\Users\\72150591\\Desktop\\新建文件夹\\node_modules',
'd:\\Users\\72150591\\Desktop\\node_modules',
'd:\\Users\\72150591\\node_modules',
'd:\\Users\\node_modules',
'd:\\node_modules'
]
},
extensions: [Object: null prototype] {
'.js': [Function (anonymous)],
'.json': [Function (anonymous)],
'.node': [Function (anonymous)]
},
cache: [Object: null prototype] {
'd:\\Users\\72150591\\Desktop\\新建文件夹\\beesapp\\b.js': Module {
id: '.',
path: 'd:\\Users\\72150591\\Desktop\\新建文件夹\\beesapp',
exports: {},
parent: null,
filename: 'd:\\Users\\72150591\\Desktop\\新建文件夹\\beesapp\\b.js',
loaded: false,
children: [Array],
paths: [Array]
},
'd:\\Users\\72150591\\Desktop\\新建文件夹\\beesapp\\a.js': Module {
id: 'd:\\Users\\72150591\\Desktop\\新建文件夹\\beesapp\\a.js',
path: 'd:\\Users\\72150591\\Desktop\\新建文件夹\\beesapp',
exports: [Object],
parent: [Module],
filename: 'd:\\Users\\72150591\\Desktop\\新建文件夹\\beesapp\\a.js',
loaded: true,
children: [],
paths: [Array]
}
}
}
requre对象中有多个属性,其中cache属性对于模块缓存具有重大的作用
(1) 模块缓存
当我们在一个项目中多次require同一个模块时,CommonJS并不会多次执行该模块文件;而是在第一次加载时,将模块缓存;以后再加载该模块时,就直接从缓存中读取该模块:
比如:// num.js
console.log('run num')
module.exports = { num: 1 }
// main.js
let number1 = require('./num');
let number2 = require('./num');
number2.num = 2;
let number3 = require('./num')
console.log(number3)
运行main.js时发现运行的结果是 // run num // 2
结论:我们多次require加载number模块,但是内部只有一次打印输出;第二次加载时还改变了内部变量的值,第三次加载时内部变量的值还是上一次的赋值,这就证明了后面的require读取的是缓存。
而该现象的出现正是由于require属性的cache实现的。
为了验证,我们可以执行 delete require.cache[d:\Users\72150591\Desktop\新建文件夹\beesapp\b.js]命令,结果就完全不一样,会打印三次run num,且number3的输出为1,
说明require每次执行都是重新执行,和之前的执行结果没有依赖关系。
( 2)加载机制
CommonJS的加载机制是,模块输出的是一个值的复制拷贝;对于基本数据类型的输出,属于复制,对于复杂数据类型,属于浅拷贝。
这两者之间的关系相当于:
(1) var a = 1; b = a,b=2,b的改变并不会导致a的值的改变,属于值的复制;
(2) var a ={a:111};b=a,b={a:222};因为是浅拷贝,两者之间指向的是同一个地址,所以两者之前的值会相互影响。
ES6
与CommonJS规范动态加载不同,ES6模块化的设计思想是尽量的静态化,使得在编译时就能够确定模块之间的依赖关系
export
和CommonJS相同,ES6规范也定义了一个JS文件就是一个独立模块,模块内部的变量都是私有化的,其他模块无法访问;不过ES6通过export关键词来导出变量、函数或者类:
这两种写法是等价的
import
使用export导出模块对外接口后,其他模块文件可以通过import命令加载这个接口
注意: import命令具有提升效果,会提升到整个模块的头部,首先执行。
export和import 都可以用as命令进行重命名
export default
在上面的import和export中都要知道具体的接口名称,当我们只需导出一个接口名称时,则可以用export default命令导出,同时,import导入模块的接口名称可以自定义,不需要和模块文件关联。
加载机制
诸如例子:
正是由于ES6模块只是输出了一个对外的接口,我们可以把这个接口理解为一个引用,实际的值还是在模块中;而且这个引用还是一个只读引用,不论是基本数据类型还是复杂数据类型
和require一样,import也会对导入的模块进行缓存,重复import导入同一个模块,只会执行一次
总结归纳
通过上面我们对CommonJS规范和ES6规范的比较,我们总结一下两者的区别:
(!) CommonJS模块是运行时加载,ES6模块是编译时输出接口
(2) CommonJS模块输出的是一个值的复制,ES6模块输出的是值的引用
(3)CommonJS加载的是整个模块,即将所有的方法全部加载进来,ES6可以单独加载其中的某个方法
(4) CommonJS中this指向当前模块,ES6中this指向undefined
(5) CommonJS默认非严格模式,ES6的模块自动采用严格模式