问: 当执行 node index.js
时(代码如下),控制台输出什么?
//index.js 文件
console.log(this === module.exports);
exports.a = 1;
module.exports.b = 2;
module.exports = {
c: 3,
};
this.d = 40;
console.log(this === module.exports);
如果上面的答案你目前还不可以很快的回答上来,那么本文会带你实现一个简易版的 require 函数,让你清晰理解 commonjs 模块化的本质。
一. 体会 commonjs 模块化的思想
-
先看一段代码:
// name.js const name="韩振方" module.export=name
-
先不看文件内容本身,就单从这个
module.export
语句来看,一开始我就感到很奇怪。我们都知道,在一个js
文件中要想使用变量,那么这个变量要么是定义好的,要么是全局对象上的一个属性。什么意思?当我运行下面代码时,控制台肯定是会报错的,理所应当,因为fancy
这个变量我压根就没声明。fancy.export= "韩振方"
-
所以我们就能想到,
module
可能会是一个全局对象上的属性,所以我才可以直接使用,于是我们写下了下面的代码,想在控制台看看module
的真面目到底是什么。结果出乎意料,这个属性竟然是undefined
!console.log('module', globalThis.module);
-
但如果我们从逆向思维去思考的话🤔,其实可以很快就否定
module
是全局对象属性这样的猜想。假如module
真的是全局变量上的一个普通属性globalThis.module
,那么各个模块之间对module
的赋值,不就相当于一层一层的替换覆盖全局变量吗?但是我们模块化本身期望的是隔离各个文件之间的变量,使其保持相对独立,互不干扰。 -
上面的
module
具体是什么,我们暂时按下不表。回顾一下js
中最创建一个最简单作用域的方法就是一个独立的{}
,像这样:const a =10 //括号内部的变量是独立的一个作用域 { const a=20 console.log('括号内部',a) } console.log('扩后外部',a) // 先后输出 20 10
-
这样的做法是做到了变量之间的相互隔离,但是我们也失去了对括号内部变量的访问手段。当我们后续代码逻辑期望获取括号内部变量时,会发现无从下手。
-
于是我们继续思考是否还有别的方法🤔,聪明的你会马上就会想到函数,我们可以通过调用函数获取返回值的方式,来访问函数作用域内部的变量。 const a = 10;
function islolated() { const a = 20; return a; } const _a = islolated(); console.log('_a', _a); console.log('a', a); // 先后输出 20 10 ,但我们可以在外部获取独立作用域内部的变量了
-
此时最核心的问题出现了,文件内部的数据倒是相互隔离了,但别的文件该如何引用这个文件的数据呢?目前我准备了两个文件
name.js
和index.js
,在index
中,我们需要通过某种方式来想办法获取到这个name.js
中的name
属性,现在我们解决的就是这个某种方式该如何来实现。// name.js const name="韩振方" -----------------文件分割线--------------------- //index.js const name = <通过某种方式获取到 name.js 的内容> console.log(name)
-
假设我们现在没有拆分
name.js
和index.js
,我们直接把name.js
的内容复制粘贴到index.js
,然后结合上面函数的作用域概念,可以写出下面的代码:function getName() { const name = '韩振方'; return name; } const name = getName(); console.log('name', name);
-
那么我们的某种方式就可以这样去设计,假设我现在有这样一个函数,它可以读取一个 js 文件内容,然后帮我执行它,然后获取它的返回值,并且重新返回一个新的返回值,这个返回值的内容就是刚刚读取到的
js
文件的返回值。 function someMethod() { // 1.读取 name.js 文件内容 // 2.执行文件代码 // 3.获取 name.js 返回值信息 // 4.重新返回 }const name = someMethod(); console.log('name', name);
-
这其实就是 commonjs模块化的核心思想,它其实是将每个
js
文件放到了一个函数的作用域中,从而实现了各个文件中的变量声明相互隔离。为了验证这一点,我们可以在在name.js
增加了一条打印语句去打印arguments
对象,可以看到这个变量竟然有值。//name.js console.log(arguments) //直接打印 arguments 对象
你可要知道这个变量是一个关键字,且只会出现在函数环境里。
-
看到这里还是有点不解?没关系,接下来我们会在
index.js
文件里实现这个someMethod
函数,届时上面所有的问题都将揭晓。
二. someMethod 函数的实现
-
我们假设已经拿到了
name.js
的代码信息。(内容比较简单,简化通过 fs 获取过程)定义一个变量code
将name.js
的内容作为字符串保存,然后使用eval
函数去执行这段代码。 function someMethod() { // 1.读取 name.js 文件内容 // 2.执行文件代码 // 3.获取 name.js 返回值信息 // 4.重新返回//这是 name.js 的代码,我们将里面的代码整体视为字符串 const code = ` const name='韩振方'; console.log('name',name); `; const result = eval(code); console.log('result',result) // 输出 name.js 代码的返回结果 return result; }
可以看到控制台正确输出了
name
,但是result
却为undefined
,这是因为name.js
目前是没有返回值的,我们还需要进一步改进。 -
整个
commonjs
最核心的思想就是下面这一步,首先定义一个中间变量result
,然后我们定一个协议:如果你这个函数需要提供给别的文件一些数据,那么你需要按照约定,使用我提前给你准备好的变量,去修改它的属性。且变量名不能做任何修改,约定的是什么就是什么。
第一步,我们在外部提前定义了一个变量
result
,然后在name.js
的内部,我们为result
增加了一个属性值name
,name.js
内部是按照约定知道外部一定会给我提供一个result
这样一个变量,所以才可以大胆的去修改。此时控制台的输出,就可以正确获取到result
的值。 -
假设我们将
result
换成module.exports
,是否感觉有点熟悉了呢?这其实就是require
函数的简易版本,在commonjs
中,module
其实就是和上面result
一样,就是一个普通对象而已,只不过在这个对象上有很多其它属性,来为模块化提供更多的细节(我们暂且不分析,只关心它的exports
属性)。下一节我们进一步分析require
真正的实现思路。
三. require 函数的实现
-
我们看一下 require 本身的使用方式
//name.js const name='韩振方' module.exports = name // 按照约定,我们修改 module.expoert 变量的属性 -------------------文件分割线--------------- index.js const moduleName = require('./name'); //然后获取 require 函数的返回值,得到 name.js 的内容
-
接下来我们实现一个简易版的
myRequire
函数,届时你会完全理解面试题的所有问题,第一步我们需要借助node
本身读取文件的能力,引入path
和fs
内置模块,去获取name.js
的文件内容。//index.js const path = require('path'); const fs = require('fs'); function myRequire(filePath) { const abPath = path.resolve(__dirname, filePath); const code = fs.readFileSync(abPath, 'utf-8'); console.log('code', code); // 打印 name.js 的代码内容 } const moduleName = myRequire('./name.js'); // 打印 name.js 的输出结果 console.log('moduleName', moduleName);
moduleName
目前是undefined
是因为我们还没有传递约定值给模块name.js
-
在这里我们定一个了一个内部函数
_run
,这个函数有三个参数,一个是exports
,一个是myRequiere
函数本身,因为name.js
有可能引用别的模块,最后一个是module
变量。特别注意⚠️: 在这里你需要理解
commonjs
模块中的module
和exports
就是一个普普通通的变量而已,不要因为他们是关键字就对它们有误解,它们真的就是一个普通的obj
对象而已。由于在
name.js
内部按照提前的约定,我们修改了module.exports
的变量属性, 此时控制台的输出,可以看到,我们在index.js
中已经获取到了name.js
使用exports
传递的属性。 -
最后一步,将使用
call
来将this
指向exports
这个变量,再加上我们常用的__dirname
和__filename
,这两个变量本身就是node
环境下可以直接访问的变量。于是一个简易的require
函数就实现了。const path = require('path'); const fs = require('fs'); function myRequire(filePath) { const abPath = path.resolve(__dirname, filePath); const code = fs.readFileSync(abPath, 'utf-8'); const module = { exports: {}, }; const exports = module.exports; function _run(exports, require, module, __dirname, __filename) { eval(code); } _run.call(exports, exports, myRequire, module,__dirname, __filename); //使用 call 将 this 指向 exports return module.exports; //返回 module.exports ,并不是 exports(!!! 注意,这里返回的是 module.exports,这一点非常关键) } const moduleName = myRequire('./name.js'); console.log('moduleName', moduleName);
四. 解答面试题
```js
console.log(this === module.exports);
//最开始 this === exports === module.exports 三者都指向同一个变量 为 true
exports.a = 1;
// 为 exports 增加 a 属性,此时 this.a === exports.a === module.exports.a 为 {a:1}
module.exports.b = 2;
// 和上面为 a 赋值一样的操作,此时 module.exports === exports = this 为 {a:1,b:2}
module.exports = {
c: 3,
};
//!!!注意:经过上面的操作, module.exports 已经指向了一块新的内存空间,已经不再是我们通过参数传递进来的那个内存空间对应的变量了。
this.d = 40;
//但是这个 this 修改的是还是原来通过参数传递的那块内存空间,此时 this === exports 为{a:1,b:2,d:40}
console.log(this === module.exports)
//两块内存空间,自然不相等 false
```
五. 思考题
上面的代码通过 require
函数引用后,得到的 result
结果为?
const result = require("./思考题.js")
console.log(result) // 结果为?