在CommonJS
中,一个文件就是一个模块,模块中的变量、函数、类都是私有的外部不可以访问,并规定module
代表当前模块,exports
是对外的接口。
CommonJS
主要依赖于module
这个类,我们可以看一下module
上面的相关属性:
Module {
id: '.', // 如果是 mainModule id 固定为 '.',如果不是则为模块绝对路径
exports: {}, // 模块最终 exports
filename: '/absolute/path/to/entry.js', // 当前模块的绝对路径
loaded: false, // 模块是否已加载完毕
children: [], // 被该模块引用的模块
parent: '', // 第一个引用该模块的模块
paths: [ // 模块的搜索路径
'/absolute/path/to/node_modules',
'/absolute/path/node_modules',
'/absolute/node_modules',
'/node_modules'
]
}
要回答这个问题我们要从CommonJS
内部执行代码的原理说起。
在CommonJS
规范中代码在运行时会被包裹在一个立即执行函数中,之后我们会改变这个立即执行函数内部this
的指向,指向的便是module.exports
这个空对象。这便可以很好的解释我们node.js中内部this指向的是一个空对象的问题。
逻辑代码:
(function (exports, require, module, __filename, __dirname) {
let name = "lm";
exports.name = name;
});
jsScript.call(module.exports, args);
之后我们会给其传递exports, require, module, __filename
等参数,所以我们可以在直接编写node.js
代码中使用这些变量。
在node.js
中我们导出一个变量、函数,或者类一般有两种到处方法。
function A() {
console.log('过年好!');
}
// 法一:module.exports.A = A;
// 法二:exports.A = A;
这两种方法有什么区别呢?其实exports
只是module.exports
的引用罢了,所以实际上这两种方法在使用上的效果是一样的。
const module = {
'exports': {
}
}
const exports = module.exports;
exports.name = 'Andy'; //完全等价于 module.exports.name = 'Andy';
所以当我们使用exports
或者module.exports
导出模块时,其实也就是给module.exports
这个对象添加属性,之后我们使用require
引入模块时得到的便是module.exports
这个对象。
注意:既然是对象属性的引用,所以当我们使用一个模块中的方法修改该模块中的变量,之后导出的变量的结果是不变的。也就是说只要一个变量已经被导出了,之后在模块内部对变量的修改都将无意义,这个情况要格外注意。(这点和ES module
有很大的不同)
a.js
let count = 1;
function add() {
count += 1;
}
exports.count = count;
exports.add = add;
b.js
let Module = require('./a');
console.log(Module.count); // 1
Module.add();
console.log(Module.count); // 1
我们在使用require
时可能是这样的:
let Module = require('./a');
如果是系统模块,或者第三方模块我们可以直接写模块名:
let fs = require('fs');
但实际上在require
模块时我们都要根据计算机中的绝对地址来引入,这个根据相对地址或者包名来查找文件的过程是比较消耗时间的,我们可以通过 module.paths
来打印一下查找的过程:
[
'c:\\Users\\dell\\Desktop\\web-design\\前端相关题目\\字节跳动\\node_modules',
'c:\\Users\\dell\\Desktop\\web-design\\前端相关题目\\node_modules',
'c:\\Users\\dell\\Desktop\\web-design\\node_modules',
'c:\\Users\\dell\\Desktop\\node_modules',
'c:\\Users\\dell\\node_modules',
'c:\\Users\\node_modules',
'c:\\node_modules'
]
所以为了提高效率,我们每次在文件中引入一个模块时,我们都会将引入的这个模块与其相应的绝对地址进行缓存,如果在一个文件中多次引入相同的模块这个模块只会被加载一次。
我们可以使用require.cache
打印出当前模块的依赖模块看看,我们可以发现其是以绝对地址为key
,模块为value
的对象:
[Object: null prototype] {
'c:\\Users\\dell\\Desktop\\web-design\\前端相关题目\\字节跳动\\b.js': Module {
id: '.',
path: 'c:\\Users\\dell\\Desktop\\web-design\\前端相关题目\\字节跳动',
exports: {},
parent: null,
filename: 'c:\\Users\\dell\\Desktop\\web-design\\前端相关题目\\字节跳动\\b.js',
loaded: false,
children: [ [Module] ],
paths: [
'c:\\Users\\dell\\Desktop\\web-design\\前端相关题目\\字节跳动\\node_modules',
'c:\\Users\\dell\\Desktop\\web-design\\前端相关题目\\node_modules',
'c:\\Users\\dell\\Desktop\\web-design\\node_modules',
'c:\\Users\\dell\\Desktop\\node_modules',
'c:\\Users\\dell\\node_modules',
'c:\\Users\\node_modules',
'c:\\node_modules'
]
},
'c:\\Users\\dell\\Desktop\\web-design\\前端相关题目\\字节跳动\\a.js': Module {
id: 'c:\\Users\\dell\\Desktop\\web-design\\前端相关题目\\字节跳动\\a.js',
path: 'c:\\Users\\dell\\Desktop\\web-design\\前端相关题目\\字节跳动',
exports: { count: 1, add: [Function: add] },
parent: Module {
id: '.',
path: 'c:\\Users\\dell\\Desktop\\web-design\\前端相关题目\\字节跳动',
exports: {},
parent: null,
filename: 'c:\\Users\\dell\\Desktop\\web-design\\前端相关题目\\字节跳动\\b.js',
loaded: false,
children: [Array],
paths: [Array]
},
filename: 'c:\\Users\\dell\\Desktop\\web-design\\前端相关题目\\字节跳动\\a.js',
loaded: true,
children: [],
paths: [
'c:\\Users\\dell\\Desktop\\web-design\\前端相关题目\\字节跳动\\node_modules',
'c:\\Users\\dell\\Desktop\\web-design\\前端相关题目\\node_modules',
'c:\\Users\\dell\\Desktop\\web-design\\node_modules',
'c:\\Users\\dell\\Desktop\\node_modules',
'c:\\Users\\dell\\node_modules',
'c:\\Users\\node_modules',
'c:\\node_modules'
]
}
}
从而可以很好的解释这个例子:
// a.js
module.exports = {
foo: 1,
};
// main.js
const a1 = require('./a.js');
a1.foo = 2;
const a2 = require('./a.js');
console.log(a2.foo); // 2
console.log(a1 === a2); // true
我们可以理解为只要模块一引入加载完,即使再次引用也还是之前的模块。
**同时缓存还很好的解决了循环引用的问题:**举个例子,现在有模块 a require模块b ;而模块b 又 require 了模块a。
// main.js
const a = require('./a');
console.log('in main, a.a1 = %j, a.a2 = %j', a.a1, a.a2);
// a.js
exports.a1 = true;
const b = require('./b.js');
console.log('in a, b.done = %j', b.done);
exports.a2 = true;
// b.js
const a = require('./a.js');
console.log('in b, a.a1 = %j, a.a2 = %j', a.a1, a.a2);
程序执行结果如下:
in b, a.a1 = true, a.a2 = undefined
in main, a.a1 = true, a.a2 = true
实际上在模块a代码执行之前就已经创建了Module实例写入了缓存,
此时代码还没执行,exports
是个空对象。
'/Users/evan/Desktop/module/a.js':
Module {
exports: {},
//...
}
}
代码exports.a1 = true;
修改了module.exports
上的a1
为true
,这时候a2
代码还没执行。
'/Users/evan/Desktop/module/a.js':
Module {
exports: {
a1: true
}
//...
}
}
进入b
模块,require a.js
时发现缓存上已经存在了,获取a
模块上的exports
。打印a1, a2
分别是true
和undefined
。
运行完b
模块,继续执行a
模块剩余的代码,exports.a2 = true;
又往exports
对象上增加了a2
属性,此时module a
的export
对象,a1, a2
均为true
。
exports: {
a1: true,
a2: true
}
再回到main
模块,由于require('./a/js')
得到的是module a
export
对象的引用,这时候打印a1, a2
都为true
。
这里还有一个需要注意的点就是,模块在加载时是同步阻塞的,只有引入的模块加载完之后才执行后面的语句,大家记住就好。
说了这么多我们主要的目的还是为了面试,所以这里小小的总结一下:
CommonJS
中一个文件就是一个模块,模块中的变量、方法、类都是私有的module
代表当前模块,module.exports
代表模块对外的接口module.exports
这个空对象,而exports
只是module.exports
的引用而已require
得到的模块中变量、方法、类的拷贝,并不是直接的引用这个是我们最常用的,我们通常会在Vue
或者Webpack
中来使用,其并像是CommonJS
那样将代码放在一个立即执行函数中(依靠闭包)从而实现模块化,而是从语法层面完成的模块化。一般情况下我们写的ES module
语法还是会通过babel
或者Webpack
等工具转化为CommonJS
语法。
对于ES module
就不详细介绍其实现原理了,主要想说一下其特点并且和CommonJS
相比有什么区别来方便大家记忆。
这点也是最重要的一点,通过上面我们知道CommonJS
是在执行到需要加载依赖模块时,会(同步阻塞)停下当前任务去加载相应的依赖模块,而对于ES module
来说无论你在哪一行引用依赖模块,其都会在一开始就进行加载相应的依赖模块。
// a.mjs
export const a1 = true;
import * as b from './b.mjs';
export const a2 = true;
// b.mjs
import { a1, a2 } from './a.mjs'
console.log(a1, a2);
在这种情况下,如果之前的CommonJS
会输出true
与undefined
,而现在会直接报错:ReferenceError: Cannot access 'a1' before initialization。
同样的原因我们在CommonJS
中可以这样写,而在ES module
中会报错:
require(path.join('xxxx', 'xxx.js'))
同样如果我们在CommonJS
中引入一个没有exports
的变量那么在代码执行时才会报错,而在ES module
在刚开始的时候就会报错。
在CommonJS
的情况下:
// counter.js
let count = 1;
function increment () {
count++;
}
module.exports = {
count,
increment
}
// main.js
const counter = require('counter.cjs');
counter.increment();
console.log(counter.count); // 1
在ES module
情况下:
// counter.mjs
export let count = 1;
export function increment () {
count++;
}
// main.mjs
import { increment, count } from './counter.mjs'
increment();
console.log(count); // 2
这一次我们导入是变量的引用了,这样可以避免之前CommonJS
在实际开发中的很多问题,实际类似于这样。
exports.counter = 1;
exports.increment = function () {
exports.counter++;
}
这个很好理解,在CommonJS
中我们加载一个模块需要将该模块的所有接口导入进来,而ES module
里我们可以按需只导入我们想要的接口。
最后顺便再提一点: 处于兼容性考虑对于像Webpack
我们在使用的ES module
时最终还是会转换为CommonJS
规范,所有有些时候我们使用require
时导入的并不是目标值,我们往往需要加一个.default
才行,这是因为ES module
的export default
语法所造成的。
其实ES module
相对于CommonJS
最大的区别就是两点:
ES module
的模块引入的变量、函数、类的引用这是很有先进性的还是值得一提的就是ES module
可以按需引入自己需要的接口,两者也是具有相同点的就是都会对已经引入的模块进行缓存,如果多次引入只会执行一次。
QQ:505417246
微信:18331092918
微信公众号:Code程序人生
个人博客:http://rayblog.ltd