exports 和 module.exports的常用方式
当我们想在一个a.js
中暴露一个常量a, 可以通过exports.a = 10
或者module.exports = {a: 10}
, b.js
引入常量a的时候:
b.js:
const { a } = require('./a'); ## 对应exports.a = 10暴露方式
console.log(a); ## 输出10
const a = require('./a').a; ## 对应module.exports = {a: 10}
console.log(a); ## 输出10
## 上面的 const a = require('./a').a 使用对象的解构赋值等价于 const { a } = require('./a');
exports 和 module.exports的区别
一个标准化解析
var module = { exports: {} };
var exports = module.exports;
return module.exports;
可以看出:
- 最终导出的是
module.exports
exports
就是module.exports
的一个引用
exports = 1 暴露出来是一个空对象?
从上面的return module.exports
, 当exports
指向了一个新的变量, 已经和module.exports
断开连接关系, module.exports
不受任何影响
var module = { exports: {} };
var exports = module.exports;
exports.a = 1; ## module.exports和exports还是同一个引用, 此时module = { exports: { a: 1} };
exports = 1; ## 此时module.exports还是指向 { a:1 }, 并不受影响
return module.exports;
同理如过modules.exports = 1
, 也会断开它和exports
之间的联系, 导致exports
失去意义, 所以一个模块只能存在一个暴露方式, 意思是module.exports
会覆盖exports
, 因为最终是返回module.exports
a.js:
export.a = 10;
module.exports = 1;
b.js:
const b = require('./a.js')
console.log(b); ## 输出 1;
模拟node的require函数实现加深理解exports 和 module.exports
来看看node对require函数的描述
require(id) id module name or path Returns: exported module content
Description: Used to import modules, JSON, and local files. Modules can be imported from node_modules. Local modules and JSON files can be imported using a relative path (e.g. ./, ./foo, ./bar/baz, ../foo) that will be resolved against the directory named by __dirname (if defined) or the current working directory.
require
函数接受一个参数, 模块名称或者路径, 返回模块的内容。require
函数用于导出模块, JSON
和本地文件。模块可以从node_modules
中导入, 本地文件和JSON
可以通过相对路径(./, ./foo,
./bar/baz, ../foo)等导出, 相对路径最终会被path
模块resolve
为文件所处位置的绝对路径。
简单模拟require的实现:
- 不考虑__filename和__dirname
- 不考虑requrei.cache缓存
- 不考虑循环引用
_require.js
function _require(dir) {
## 定义一个module对象
var module = {
exports: {}
};
## 引入nodejs 文件模块 下面是nodejs中原生的require方法
var fs = require('fs');
var path = require('path');
## 同步读取该文件
var moduleContent = fs.readFileSync(path.resolve(__dirname, dir), 'utf8');
## 处理本地文件为.json
var ext = path.extname(path.resolve(__dirname, dir));
var addModuleExports = ext === '.json' ? 'module.exports=' : '';
## 头尾拼接包装成新的字符串
var packFuncStr = '(function(module,exports){ ' + addModuleExports + moduleContent + '};)';
## 字符串转换成包装函数
var packFunc = eval(packFuncStr);
## 把上面声明module和它内部的module.exports都作为参数传进去
packFunc.call(module.exports, module, module.exports);
## packFunc第二个参数已经指明了exports = module.exports
## packFunc执行后上面的声明的modeule对象得到 moduleContent 中挂载到module.exports 或 exports上的API
return module.exports;
# 返回module.exports, 最终我们拿到了path代表的文件模块暴露的API
}
module.exports = _require;
测试 _require
demo
|____ a.js
|____ b.js
|____ _require.js
|____ c.json
a.js:
export.a = 10;
c.json:
{
"name": 'tftoy'
}
b.js:
const _require = require('./_require.js');
const { a } = _require('./a.js');
const c = _require('./c.json');
console.log(a); ## 输出10;
console.log(c); ## 输出{ name: 'tftoy' };
// 测试结果, 符合预期
我们来分析下整个过程
当_require('./a.js')
发生以下事情:
fs
模块读取文件a.js- 拼接成包装函数字符串
eval
转换为包装函数- 执行包装函数
- 返回
module.exports
_require('./a.js')
module = {
exports: {}
};
## 文件内容
moduleContent = 'exports.a = 10;';
## 文件后缀
ext = '.js'
addModuleExports = '';
## 包装函数字符串
packFuncStr = '(function (module,exports){ ' + '' + 'exports.a = 10;' + '};)'
## 包装函数
packFunc = function (module,exports) { exports.a = 10;};
## 执行包装函数
packFunc.call(module.exports, module, module.exports);
## 返回结果
return { a: 10 };
因此_require('./a.js') => { a: 10 };
同理_require('./c.json')
也是这么一个过程。
总结
- 介绍了
exports
和module.exports
的用法 exports
和module.exports
的关系、区别- 简单实现了
require
函数
上述_require只是实现了一些基本功能, 还有很多场景没有考虑, 感兴趣的可以去看看webpack对require函数的实现, 它可以处理CommonJs模块和Es6模块, 但是require函数的思想是差不多的, 模块最后都是被装在一个函数字符串中
eval("__webpack_require__(/*! ./index.js */)
中。