总有一些似乎没什么用但总是记不住的面试题,比如这道题:
// a.js
exports.c = 3;
module.exports = {
a: 1,
b: 2,
};
this.m = 5;
// b.js
const a = require('./a');
console.log(a);
这个模块导出什么?
如果只看上面代码中的 exports
和 module.exports
,毫无疑问module.exports
的导出会生效。大概能猜出最后输出的是{ a: 1, b: 2}
。不过,这个 this 又是啥?他们之间是怎么决定使用哪个的?
exports 、 module.exports 和 this 的区别
在 Node.js
中,exports
和 module.exports
都用于模块化导出,但它们有一些重要区别:
module.exports
module.exports
是模块的输出。
使用 module.exports
实际上是在替换模块的导出对象,而不是添加属性到现有的对象。
// a.js
exports.c = 3;
module.exports = {
a: 1,
b: 2,
};
// b.js
const a = require('./a');
console.log(a); // { a: 1, b: 2}
即使换成下面代码,依旧是module.exports
生效。
// a.js
module.exports = {
a: 1,
b: 2,
};
exports.c = 3;
所以在实践中,通常更推荐使用 module.exports
来保持一致性。
exports
exports
是module.exports
的一个引用,在模块加载时,require
返回的是module.exports
,而不是exports
。
使用 exports
时,只是在修改 module.exports
指向的对象,而不是替换它。
// a.js
exports.a = 1;
exports.b = 2;
exports.c = 3;
// b.js
const a = require('./a');
console.log(a); // { a: 1, b: 2, c: 3 }
也可以说,在不使用 module.exports 的情况下,exports.c = 3
实际上是相当于 module.exports.c = 3
。
this
this
在模块中指向 module.exports
,因此你可以使用 this
来添加属性到模块的导出对象。
把上面的示例修改一下:
// a.js
exports.a = 1;
this.m = 5;
// b.js
const a = require('./a');
console.log(a); // { c: 3, m: 5 }
this.m = 5
实际上是在为 module.exports
添加一个属性 m
,这相当于 module.exports.m = 5
。
在最开始的题目中,重赋值 module.exports
之后,this 也指向新的 module.exports
。所以输出结果没有m
。
导入另一个模块时发生了什么
下面伪代码模拟了 Node.js 中 require
函数的实现,以帮助理解模块导入的过程。
function require(modulePath) {
//1. 将modulePath转换为绝对路径:
//2. 判断是否该模块已有缓存 (cache为require的一个属性)
// if(require.cache["D:\\路径\\a.js"]){
// return require.cache["D:\\路径\\a.js"].result;
// }
//3. 读取文件内容
//4. 包裹到一个函数中
function __temp(module, exports, require, __dirname, __filename) {
exports.c = 3;
module.exports = {
a: 1,
b: 2,
};
this.m = 5;
}
//6. 创建module对象 (需调用函数)
module.exports = {}; // 先创建空对象
const exports = module.exports; // 把module.exports赋值给exports
// 调用函数时,把module.exports当作this绑定
__temp.call(module.exports, module, exports, require, module.path, module.filename);
// 最终返回的是module.exports
return module.exports;
}
require.cache = {};
在步骤 6 中,__temp.call(module.exports, module, exports, require, module.path, module.filename)
这行代码将 module.exports
作为 this
绑定到 __temp
函数中
// a.js
console.log(this === exports); // true
console.log(this === module.exports); // true
this
, exports
, module.exports
在一开始是一样的,都是同一个对象。
exports.c = 3;
相当于在对象中加入属性 c, 此时 this 也有 c
而 module.exports = { a: 1, b: 2};
相当于把 this 指向新的对象,所以 this.m = 5; 没有生效。
输出过程展示:
// a.js
console.log(this === module.exports); //true
console.log(this === exports); //true
console.log(this); // {}
console.log(exports); // {}
console.log(module.exports); // {}
exports.c = 3;
module.exports = {
a: 1,
b: 2,
};
this.m = 5;
console.log(this === module.exports); // false
console.log(this === exports); // true
console.log(this); // { c: 3, m: 5 }
console.log(exports); // { c: 3, m: 5 }
console.log(module.exports); // { a: 1, b: 2 }
// 导出改模块,最终输出为 { a: 1, b: 2 }
总结
-
exports
和module.exports
是用来定义模块的导出内容。exports
是module.exports
的一个引用,但如果module.exports
被重新赋值,exports
的引用将被忽略。 -
this
在模块文件的顶层上下文中,指向module.exports
。在模块开始时,this
,exports
和module.exports
是同一个对象的引用。但重新赋值module.exports
时指向一个新的对象,exports
和this
仍然指向旧对象。
综合来说:
-
module.exports
:最终决定模块导出的内容。在这个示例中,它会被设置为{ a: 1, b: 2 }
,exports.c
和this.m
都不会影响到最终的导出。 -
exports.c = 3
:不会生效,因为module.exports
已经被重新赋值了。 -
this.m = 5
:也不会影响最终的导出,因为module.exports
被重新赋值了。