模块是Node.js里面一个很基本也很重要的概念,各种原生类库是通过模块提供的,第三方库也是通过模块进行管理和引用的。本文会从基本的模块原理出发,到最后我们会利用这个原理,自己实现一个简单的模块加载机制,即自己实现一个
require
。本文完整代码已上传GitHub:https://github.com/dennis-jiang/Front-End-Knowledges/blob/master/Examples/Node.js/Module/MyModule/index.js
简单例子
老规矩,讲原理前我们先来一个简单的例子,从这个例子入手一步一步深入原理。Node.js里面如果要导出某个内容,需要使用
module.exports
,使用module.exports
几乎可以导出任意类型的JS对象,包括字符串,函数,对象,数组等等。我们先来建一个a.js
导出一个最简单的hello world
:// a.js module.exports = "hello world";
然后再来一个
b.js
导出一个函数:// b.js function add(a, b) { return a + b; } module.exports = add;
然后在
index.js
里面使用他们,即require
他们,require
函数返回的结果就是对应文件module.exports
的值:// index.js const a = require('./a.js'); const add = require('./b.js'); console.log(a); // "hello world" console.log(add(1, 2)); // b导出的是一个加法函数,可以直接使用,这行结果是3
require会先运行目标文件
当我们
require
某个模块时,并不是只拿他的module.exports
,而是会从头开始运行这个文件,module.exports = XXX
其实也只是其中一行代码,我们后面会讲到,这行代码的效果其实就是修改模块里面的exports
属性。比如我们再来一个c.js
:// c.js let c = 1; c = c + 1; module.exports = c; c = 6;
在
c.js
里面我们导出了一个c
,这个c
经过了几步计算,当运行到module.exports = c;
这行时c
的值为2
,所以我们require
的c.js
的值就是2
,后面将c
的值改为了6
并不影响前面的这行代码:const c = require('./c.js'); console.log(c); // c的值是2
前面
c.js
的变量c
是一个基本数据类型,所以后面的c = 6;
不影响前面的module.exports
,那他如果是一个引用类型呢?我们直接来试试吧:// d.js let d = { num: 1 }; d.num++; module.exports = d; d.num = 6;
然后在
index.js
里面require
他:const d = require('./d.js'); console.log(d); // { num: 6 }
我们发现在
module.exports
后面给d.num
赋值仍然生效了,因为d
是一个对象,是一个引用类型,我们可以通过这个引用来修改他的值。其实对于引用类型来说,不仅仅在module.exports
后面可以修改他的值,在模块外面也可以修改,比如index.js
里面就可以直接改:const d = require('./d.js'); d.num = 7; console.log(d); // { num: 7 }
require
和module.exports
不是黑魔法我们通过前面的例子可以看出来,
require
和module.exports
干的事情并不复杂,我们先假设有一个全局对象{}
,初始情况下是空的,当你require
某个文件时,就将这个文件拿出来执行,如果这个文件里面存在module.exports
,当运行到这行代码时将module.exports
的值加入这个对象,键为对应的文件名,最终这个对象就长这样:{ "a.js": "hello world", "b.js": function add(){ }, "c.js": 2, "d.js": { num: 2 } }
当你再次
require
某个文件时,如果这个对象里面有对应的值,就直接返回给你,如果没有就重复前面的步骤,执行目标文件,然后将它的module.exports
加入这个全局对象,并返回给调用者。这个全局对象其实就是我们经常听说的缓存。**所以require
和module.exports
并没有什么黑魔法,就只是运行并获取目标文件的值,然后加入缓存,用的时候拿出来用就行。**再看看这个对象,因为d.js
是一个引用类型,所以你在任何地方获取了这个引用都可以更改他的值,如果不希望自己模块的值被更改,需要自己写模块时进行处理,比如使用Object.freeze()
,Object.defineProperty()<
深入Node.js的模块加载机制(手写require函数)
最新推荐文章于 2025-07-16 15:56:06 发布