什么是模块?关键词(数据私有,暴露接口)
公共的js
文件,即将一个复杂的程序依据一定的规则封装成几个块文件,并组合在一起,模块的内部数据、实现是私有的,只是向外部暴露一些接口(方法)与外部其他模块通信。
原始模块化写法:
在没有CommonJS
和ES6
之前的时候,想要达到模块化的效果有这么3种。
一个函数就是一个模块
function m1{
//...
}
function m2{
//...
}
复制代码
缺点:污染了全局变量,无法保证不会与其他模块发生冲突,而且模块成员之间看不出直接关系。
一个对象就是一个模块
为了解决函数模块的缺点,可以把模块写成一个对象,所有的模块成员都放到这个对象里面。
var module1 = new Object({
_sum:0,
foo1:function(){},
foo2:function(){},
})
复制代码
缺点:会暴露所有模块成员,内部的状态可能被改写。例如我们如果只想暴露两个方法foo
而不暴露出_sum
就做不到,而此时可以通过 module1. _sum=2
,来改写该内部属性。
立即执行函数为一个模块
var module = (function(){
var _sum = 0
var foo1 = function(){}
var foo2 = function(){}
return {
foo1:foo1,
foo2:foo2
}
})()
复制代码
利用立即执行函数内的作用域以及闭包来实现模块化功能,导出我们想要的成员,此时外部就不能访问到 _sum
属性了。
Node中模块的导入与导出
node
采用的commonjs
的模块系统, 导入模块用的是require
这个全局函数,导出模块用的是module.exports
的方式,
require
方法导入本地的某个文件组件的话, 一定要加上盘符前缀(即便是在同一目录下)。
以对象形式导出如下:
以对象形式导入如下:
直接导出如下:
直接导入如下:
module对象
Node
内部提供一个Module
构造函数。所有模块都是Module
的实例,如上图所示。
每个模块内部,都有一个module对象,代表当前模块。如上图所示,它有以下属性:
module.id
模块的识别符,通常是带有绝对路径的模块文件名。
module.filename
模块的文件名,带有绝对路径。
module.loaded
返回一个布尔值,表示模块是否已经完成加载。
module.parent
返回一个对象,表示调用该模块的模块。
module.children
返回一个数组,表示该模块要用到的其他模块。
module.exports
表示模块对外输出的值。
module.parent
里面的Module.paths
是查找导入模块路径顺序的优先级,如果在这一级中没有找到,就依次往上级查找,直到找到或者找不到为止。
exports变量
为了方便,Node为每个模块提供一个exports
变量,指向module.exports
。这等同(只是等同于,并不是真的有这行代码)在每个模块头部,有一行这样的命令:let exports = module.exports;
这样造成的结果是,在对外输出模块接口时,可以向exports
对象添加方法,如下:
// 代码示例
// 注意是在exports对象上添加了一个方法
exports.area = function(r){
return Math.PI * r * r;
}
// 注意是在exports对象上添加了一个方法
exports.circumference = function(r){
return 2 * Math.PI * r;
}
复制代码
注意,不能直接将exports
变量指向一个值,因为这样等于切断了exports
与module.exports
的联系,以下是错误示范
:
// 要注意这是错误的写法!因为这里直接将exports指向了一个新的值,切断了exports与原来module.exports的联系
exports = function(x){
console.log(x);
}
复制代码
require指令
require命令
的基本功能是,读入并执行一个JavaScript文件
,然后返回该模块的exports
所导出的对象。如果没有发现指定模块,则会得到一个空对象。
如下图所示:
require命令
用于加载文件,后缀名默认为.js
。
通常,我们会把相关的文件会放在一个目录里面,便于组织。这时,最好为该目录设置一个入口文件,让require方法
可以通过这个入口文件,加载整个目录。
require
发现参数字符串指向一个目录以后,会自动查看该目录的package.json文件
,然后加载main字段
指定的入口文件;如果package.json文件
没有main字段
,或者根本就没有package.json文件
,则会加载该目录下的index.js文件
或index.node文件
。
例如下图:
在上图中:
第1步,我们先在service.js文件
中使用require方法
,找到当前目录下的tool文件夹
下的math文件夹
第2步,自动查看math文件夹
下的package.json文件
第3步,通过package.json文件
中的main字段
,找到指定的入口文件,即当前目录下的index.js文件
第4步,所以在第1步中let add = require('./tool/math')
,就相当于是let add = require('./tool/math/index.js')
,即service.js文件
中require
导入并赋值给变量add
保存的对象,是从index.js文件
中所exports
导出的对象{add:[Function:add]}
第5步,所以在终端中node
执行service.js文件
,打印输出的就是对象{add:[Function:add]}
根据参数的不同格式,require命令
去不同路径寻找模块文件。
(1)如果参数字符串以“/”
开头,则表示加载的是一个位于绝对路径
的模块文件。比如,require('/home/marco/foo.js')
将加载/home/marco/foo.js
(2)如果参数字符串以“./”
开头,则表示加载的是一个位于相对路径
(跟当前执行脚本的位置相比)的模块文件。比如,require('./circle')
将加载当前脚本同一目录的circle.js
(3)如果参数字符串不以“./”
或“/”
开头,则表示加载的是一个默认提供的核心模块
(位于Node的系统安装目录中),或者一个位于各级node_modules
目录的已安装模块(全局安装或局部安装)
(4)如果参数字符串不以“./”
或“/”
开头,而且是一个路径,比如require('example-module/path/to/file')
,则将先找到example-module
的位置,然后再以它为参数,找到后续路径
(5)如果指定的模块文件没有发现,Node
会尝试为文件名添加.js
、.json
、.node
后,再去搜索。.js
件会以文本格式的JavaScript
脚本文件解析,.json
文件会以JSON
格式的文本文件解析,.node
文件会以编译后的二进制文件解析
(6)如果想得到require
命令加载的确切文件名,使用require.resolve()
方法
模块的补充信息
模块的缓存
第一次加载某个模块时,Node会缓存该模块。以后再加载该模块,就直接从缓存取出该模块的module.exports
属性。
上面代码中,连续两次使用require
命令,加载同一个模块。第一次加载的时候之后,为输出的对象添加了一个a
属性,值为1
。但是第二次加载的时候,这个a
属性依然存在,这就证明require
命令并没有重新加载模块文件,而是输出了缓存
。
其原因就是因为在第一次require
导入模块时,会得到一个对象,后续再require
导入同一个模块时,会检测是否已经引用过了,如果已经存在了一个之前导入过的对象,后续的引用就直接拿到该对象进行赋值,而对象的赋值,是引用类型
的赋值,所以指向的都是同一个对象。
如果想要多次执行某个模块,可以让该模块输出一个函数
,然后每次require
这个模块的时候,重新执行
一下输出的函数
。
删除模块缓存
所有缓存的模块保存在require.cache
之中,如果想删除模块的缓存,可以像下面这样写:
(注意,缓存是根据绝对路径识别模块的,如果同样的模块名,但是保存在不同的路径,require命令还是会重新加载该模块。)
循环加载(引用)问题
如果发生模块的循环加载,即A加载B,B又加载A,则B将加载A的不完整版本,如下图:
完整解析如下:
service.js
内代码如下:
console.log("service.js",require("./tool/a"))
// 第一步执行上面这行代码,但是需要require获取到a.js文件的数据,所以先跳转到a.js文件中去执行
// 从第七步跳转回来,带着刚刚从a.js文件中获取的exports.x的值为"a2",继续上面这行代码在控制台输出打印输出:service.js {x:"a2"}
console.log("service.js",require("./tool/b"))
//第八步执行上面这行代码,打印输出:service.js {x:"b2"}
复制代码
a.js
内代码如下:
exports.x = "a1" // 第二步,跳转到a.js文件的第一行代码,此时exports.x的值为"a1"
console.log("a.js",require("./b.js"))
// 第三步,紧接着执行上面这行代码,但是这行代码中需要require获取到b.js的数据,所以又需要先跳转到b.js文件
// 从第六步跳转回来,带着刚刚从b.js文件中获取的exports.x的值为"b2",此时上面这行代码在控制台输出打印:a.js {x:"b2"}
exports.x = "a2" // 第七步,继续执行这行代码,将exports.x的值赋值为"a2",此时a.js文件中所有代码都执行完了,service.js文件中第一步代码所需要获取值也都能全部获取到了,此时再跳转回service.js文件执行剩下的代码
复制代码
b.js
代码如下:
exports.x = "b1" // 第四步,跳转到b.js文件的第一行代码,此时exports.x的值为"b1"
console.log("b.js",require("./a.js"))
// 第五步,紧接着执行上面这行代码,但是这行代码中需要require获取到a.js的数据,所以又得跳回a.js文件获取其中exports.x的值,但是a.js文件中执行的代码依旧停留在第三步,我们要获取a.js文件中exports.x的值也只能往上面代码第二步找,第二步中exports.x的值为"a1" 。所以这个时候b.js文件的这行代码只能拿到a.js文件中exports.x的值为"a1",所以在控制台上最先console打印输出:b.js {x:"a1"}
exports.x = "b2" // 第六步,执行这行代码,将exports.x的值赋值为"b2",此时b.js文件中所有代码都执行完了,a.js文件中第三步代码所需要获取值也都能全部获取到了,此时再跳转回a.js文件执行剩下的代码
复制代码
模块的运行机制
模块的加载机制:
CommonJS
模块的加载机制是,输入的是被输出的值的拷贝
。也就是说,一旦输出一个值,模块内部的变化就影响不到这个值(记住仅仅只是普通的值
, 要是引用类型
的值的话,还是受影响的
)。普通数据类型
的模块加载就是值类型
,直接复制这个值,发生变化不会影响原来的值
,而引用数据类型
的值的模块加载就是引用该值的引用地址
,发生变化后会影响原来的值
。请看下图中的例子:
require的内部处理流程:
require
命令是CommonJS
规范之中,用来加载其他模块的命令。它其实不是一个全局命令,而是指向当前模块的module.require
命令,而后者又调用Node
的内部命令Module._load
。如下所示:
上面的第4步,采用module.compile()
执行指定模块的脚本,逻辑如下:
上面的第1步和第2步,require
函数及其辅助方法主要如下:
1、require()
: 加载外部模块
2、require.resolve()
:将模块名解析到一个绝对路径
3、require.main
:指向主模块
4、require.cache
:指向所有缓存的模块
5、require.extensions
:根据文件的后缀名,调用不同的执行函数