前言
Hello~大家好。我是秋天的一阵风
最近在复习nodejs
相关知识的时候碰到一道面试题让我虎躯一震。对于commonjs
,以前只是零零散散记得它的一些特点,比如:
CommonJs
有缓存,每次require才去加载,已经加载过的不再加载,CommonJs
是同步加载的,AMD和CMD是异步加载的.module.exports = value;
一般是整个对象一起赋值:module.exports = {}
,exports.xxx =value
一般是赋值单个变量:exports.name = name
exports.xxx = value
是一种简便写法,不能够直接赋值一个对象
但是对于commonJs
的原理并没有去深入的理解,比如为什么commonJs
会有缓存,为什么可以有两种导出方式?使用exports
时为什么不能直接赋值一个对象呢?
其实这几个问题是非常值得我们花时间去探究的,现在刚好遇到这道非常经典的面试题,借这个契机,让我们来彻底弄懂其中的答案,好了,话不多说,我们来看看这道面试题。
先留给大家两分钟时间,请同学们开始思考下在node
环境下,index.js
文件中会打印什么呢?
// 1.js
this.a = 1;
exports.b = 2;
exports = { c: 3 };
module.exports = { d: 4 };
exports.e = 5;
this.f = 6;
// index.js
const r = require("./1.js");
console.log(r);
要想知道最终答案,其实关键在于这个require
函数,只要知道require
函数内部做了什么,答案就清晰了。
我们来简单看下require
函数的实现。
一、require函数伪代码
function require(modulePath) {
//1.根据传递的模块路径,得到模块完整的绝对路径
var moduleId = getModuleId(modulePath);
//2. 判断缓存
if (cache[moduleId]) {
return cache[moduleId];
}
//3.真正运行模块代码的辅助函数
function _require(exports, require, module, __filename, __dirname) {
//目标模块的代码在这里
}
// 4. 准备并运行辅助函数
var module = {
exports: {},
};
var exports = module.exports;
// 得到模块文件的绝对路径
var __filename = moduleId;
// 得到模块所在目录的绝对路径
var __dirname = getDirName(__filename);
_require.call(exports, exports, require, module, __filename, __dirname);
// 5. 缓存 module.exports
cache[moduleId] = module.exports;
// 6. 返回 module.exports
return module.exports;
}
二、运行步骤分析
-
首先
require
函数会接受一个modulePath
参数,也就是我们传入的文件路径"./1.js"
。根据这个文件路径得到一个唯一的模块ID,这个ID是什么呢?其实就是这个模块的完整绝对路径。 -
根据模块ID判断
cache
中是否已经存在,如果缓存中已经有了,就直接返回缓存结果。这也就解释了文章开头我们提出的第一个问题:为什么commonjs
是有缓存的。 -
接下来就是运行函数了,怎么运行呢?其实就是将
1.js
文件中全部代码放到_require函数里面去执行,也就是下面的形式:
function _require(exports, require, module, __filename, __dirname) {
//目标模块的代码在这里
this.a = 1;
exports.b = 2;
exports = { c: 3 };
module.exports = { d: 4 };
exports.e = 5;
this.f = 6;
}
好奇的同学可能会提问:怎么证明1.js
文件里的代码是在函数里面运行的呢?
我们可以在1.js
里面打印 arguments
,arguments
只有在函数内部才有值。
// 1.js
console.log(arguments)
this.a = 1;
exports.b = 2;
exports = { c: 3 };
module.exports = { d: 4 };
exports.e = 5;
this.f = 6;
打印结果:
- 好了,我们继续探究,这个
_require
函数在执行的时候还有传入5个函数分别是:exports, require, module, __filename, __dirname
你可以打印arguments
的长度来验证:
// 1.js
console.log("arguments.length: ",arguments.length) //arguments.length: 5
this.a = 1;
exports.b = 2;
exports = { c: 3 };
module.exports = { d: 4 };
exports.e = 5;
this.f = 6
- 第一个参数
exports
是不是很熟悉呢? 这也就是为什么我们在js文件中可以直接使用exports
的原因,比如:
// 1.js
exports.b = 2;
exports = { c: 3 };
exports.e = 5;
- 第二个参数
require
大家应该也会熟悉,这也就是为什么我们在js文件中可以直接使用require进行导入的原因,比如:
// index.js
const r = require("./1.js");
console.log(r);
至于剩下的三个参数:module, __filename, __dirname
,我们就不再赘述了。
三、函数执行
我们继续探究,到了最关键的一步,也就是运行辅助函数,它会传入一个module
对象,这个module
对象有一个属性exports
,值也是一个对象。除此之外,还会将这个exports
单独提出来,并且获取模块文件的绝对路径__filename
和模块所在目录的绝对路径__dirname
,最后通过call
方法指定exports
对象为this
来执行。
// 4. 准备并运行辅助函数
var module = {
exports: {},
};
var exports = module.exports;
// 得到模块文件的绝对路径
var __filename = moduleId;
// 得到模块所在目录的绝对路径
var __dirname = getDirName(__filename);
_require.call(exports, exports, require, module, __filename, __dirname);
// 5. 缓存 module.exports
cache[moduleId] = module.exports;
// 6. 返回 module.exports
return module.exports;
我们来在1.js
文件中打印一些log进行验证:
console.log("module.exports: ",module.exports) // {}
console.log("exports === module.exports: ",exports === module.exports) // true
console.log("this === exports: ",this === exports); // true
console.log("this === module.exports: ",this === module.exports) // true
this.a = 1;
exports.b = 2;
exports = { c: 3 };
module.exports = { d: 4 };
exports.e = 5;
this.f = 6;
可以看到,module.exports
在一开始是个空对象,而this
指向跟exports
和module.exports
是完全相等的。
四: 回到笔试题中
好了,搞清楚了具体的执行逻辑,我们现在重新回到笔试题中来尝试寻找答案。
this.a = 1;
exports.b = 2;
exports = { c: 3 };
module.exports = { d: 4 };
exports.e = 5;
this.f = 6;
一开始的时候,this
、exports
、module.exports
都是空对象 this = exports = module.exports = {}
this.a = 1;
后变成:
this = exports = module.exports = { a:1 }
2. exports.b = 2;;
后变成:
this = exports = module.exports = { a:1, b:2 }
3. exports = { c: 3 };
,关键的来了,这里是给exports
对象重新赋值了
this = module.exports = { a:1, b:2 }
exports = { c: 3 }
这个时候exports
和this
、module.exports
已经不相等了。
module.exports = { d: 4 };
,同样的,module.exports
也被重新赋值了
this = { a:1, b:2 }
exports = { c: 3 }
module.exports = { d: 4 }
5. exports.e = 5;
后变成:
this = { a:1, b:2 }
exports = { c: 3, e:5 }
module.exports = { d: 4 }
6. this.f = 6;
后变成:
this = { a:1, b:2, f:6}
exports = { c: 3, e:5 }
module.exports = { d: 4 }
我们可以打印一些结果验证:
- 最后的最后,一定要注意最后一句导出的代码:
// 伪代码
// ..
// 6. 返回 module.exports
return module.exports;
返回的不是this
,也不是exports
,而是 module.exports
所以最终的答案就显而易见了:
const r = require("./1.js");
console.log(r);// { d: 4 }