更多个人博客:(https://github.com/zenglinan/blog)
如果对你有帮助,欢迎star。
在 JavaScript 诞生之际, 人们将 JavaScript 当做网页的小脚本语言, JavaScript 里缺乏模块化的概念。 后来, CommonJS 规范给 JavaScript 提供了一系列参照, 包括了模块化, 二进制, Buffer, I/O流等。
Node.js 正是参照了 CommonJS, 实现了模块化。
在 Node.js 中, 每个 js 文件都看做一个模块, 每个模块上有两个核心模块对象
module
-
require
接下来, 分别说说他们是怎么回事
一. module 对象
我们先打印一下看看 module
对象里有什么
我们来一个个掰扯一下这些属性是什么意思
1. id
每个模块都有一个唯一确定的 id
, 用于区分模块, id
一般为文件的绝对路径。也就是说, 模块和硬盘上的文件是相对应的。
什么? 你说显示的是 '.' , 不是绝对路径?
因为假如打印的模块正是 node xxx
对应的模块(文件), 就会用 '.' 代替。
2. exports
exports
是一个十分重要的属性, 他上面挂载了模块导出的内容, 我们可以通过如下的方式导出一个变量或方法
exports.a = 1
exports.fn = function(){}
或许之前你都是这样导出的:
// 这样
module.exports = function(){}
// 或是这样
module.exports = {
get: function(){},
a: 1
}
其实都可以的 ~ 因为 exports
实际上和 module.exports
指向的是同一个对象, 可以打印一下:
console.log(module.exports === exports) // true
正因为如此, 我们不能直接给 exports
赋值
exports = function(){}
这会导致 exports
丢失对 module.exports
的引用
另外, 当我们 require
的时候, 实际上引入的就是模块的exports
// child
exports.a = 1
// main
console.log(require('./child')) // {a: 1}
3. parent && children
我们在 main 模块里引入 child 模块, 并打印 module
const child = require('./child')
console.log('in main', module)
同时在被引入的 child 模块中也打印一下 module
module.exports = {
a:1
}
console.log('in child', module)
这里可以看到, main
模块调用了 child
, child
和 main
为父子模块关系.
这里我们可以发现: 为什么父模块的子模块的parent
会显示为[Circular]
呢? 父亲的儿子的父亲不应该就是父亲吗? 为什么显示 [Circular]
哈哈, 是不是有点绕口, 事实上, 这里不显示正是因为这是个循环引用的关系, 假如要显示父模块, 那父模块里面还有children
呢, children
里面还可以显示 parent
, 这就绕不出来了.
4. filename
顾名思义, 模块的文件名, 只不过是以绝对路径显示的 O(∩_∩)O
5. loaded
用于显示模块是否已经完成加载
6. paths
module.path
里保存了从当前目录, 一直找到根目录下, 所有的 node_modules 文件夹的路径
这个 path
属性的作用是:
当导入自定义模块时, 如果不指定路径, 用 require(xxx)
这种方式引入的话, Node 会去遍历当前执行文件的 module.paths
来查找这个模块.
如果找不到, 就会抛出错误.
Cannot find module 'xxx'
注意: module.paths
是一个数组, 查找模块的时候会从第一个路径找起, 也就是说:
假如多个路径下都有该模块, node 会优先使用 paths
中靠前的路径下的模块
接下来, 再来说说 reqire
对象
二. require
老规矩, 先打印看看
可以看到, require
实际上是一个函数对象
除了可以通过 require('xxx')
调用这个函数之外, 还可以访问这个函数对象上的一些属性.
下面我们来捋一捋这几个属性
1. resolve
require.resolve
也是一个方法, require.resolve('xxx')
和 require('xxx')
类似, 只不过:
前者会去执行 xxx 模块, 后者只会解析不会执行.
require.resolve('xxx')
返回的是 xxx 模块的绝对路径.
2. main
方眼一看, 这个....怎么和上面的 module
这么像, 我们大胆推测下:
会不会 module
是 resolve
对象上的一个属性
答案: 是的~
console.log(require.main === module) // true
是不是感觉两个对象一下子充满了联系(▽)
3. extensions
如果你有去打印一下, 你会发现每个 module
里都有这个属性, 而且长得一样一样的
这个属性实际上写明了 Node.js 支持的 require 的文件类型: .js .json .node
如果没有写明扩展名的话, Node.js 会依次尝试去寻找 .js > .json > .node
文件, 所以为了保证 Node.js 的加载速度, 当引入一个非 js 文件时, 应该注明扩展名.
4. cache
node 在加载执行模块的时候, 都会对模块进行缓存.
注意:
require.resolve
的模块不会进行缓存.
cache
属性里保存了模块的缓存, 缓存的内容就是模块的 module
对象
仔细看, 里面居然还有 id: '.'
的模块, 还记得我们最开始说的什么吗?
node 执行的模块的 id 会显示为 '.'
对的, cache 不但会缓存子模块的 module
对象, 还会将模块本身的 module
对象也缓存了.
有了缓存之后, 如果我们重复 require
一个模块, 这个模块的内容只会被执行一次
// child
console.log(1)
// main
require('./child') // 1
require('./child') // 不显示任何内容
如果希望重新 require
时, 模块依旧会被执行, 可以手动清除缓存
不过, 最好的办法, 是将 child 模块包装成一个函数返回
// child
module.exports = function(){
console.log(1)
}
// main
require('./child')() // 1
require('./child')() // 1
番外: 全局包裹函数
实际上, 在 Node.js 中, 每一个模块都用一个函数包裹起来了, node 在执行模块的时候, 并不会直接执行模块代码, 而是执行这个函数包裹器.
这个函数大概长这样:
(function(exports, require, module, __filename, __dirname) {
// 模块内部的代码
// 返回值为 module.exports, 且返回值无法手动修改
})
看看这上面我们熟悉的参数, exports, require, module
, 这也就解释了我们为什么可以使用这些未声明的全局变量了, 实际他们都是 node 传给包裹函数的参数
来看看两个新的全 (函) 局 (数) 变 (参) 量 (数) :
__dirname 返回当前路径 (不包含文件名)
__filename 返回当前文件路径 (包含文件名)
包裹函数的好处不限于此, 我们可以思考一下: 当我们用 JavaScript
中的 标签引入别的 js 文件时, 全局变量是不是会互相干扰.
node 中的包裹函数就规避了作用域污染, 任何模块中的全局变量, 经过了包裹器的包裹, 在其他模块中, 都成了局部变量。
参考:
- 《深入浅出Node.js》
- Requiring modules in Node.js: Everything you need to know