exports, module.exports和this 同时设置,最终导出的是什么
总有一些似乎没什么用但总是记不住的面试题,比如这道题:
// 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.exportsreturn 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
被重新赋值了。