node-模块化

前言

之前学习node的时候,没有从官方文档去入手。虽然在用exports和module.exports,网上也了解到一些他们之间的关系,但是当时想象不到内部是怎么实现的。今天看了一下官方的module模块,很多疑问豁然开朗。在此纪念自己走过的那么多弯路。
下面总结的内容,不适合js基础不好的同学。起码得知道js里面的作用域链,js里面面向对象是怎么实现的,还需要了解js语言的内存模型。

Module

官方网址:https://nodejs.org/docs/latest/api/modules.html#
摘录重点:

  • 1、在 Node.js 模块系统中,每个文件都被视为独立的模块。
  • 2、模块内的本地变量是私有的,因为模块被 Node.js 包装在一个函数中
    1
    2
    3
    (function(exports, require, module, __filename, __dirname) {
    // 模块的代码实际上在这里
    });
    这样也就可以解释,为什么我们可以在每个文件中,调用require,module,__filename等了,因为这些都是包装函数里面的参数呀。
    那为什么每个文件的本地变量是私有的呢?因为函数具有独立作用域呀。
  • 3、module.exports和exports
    我们经常会看到这样的代码:
    1
    2
    3
    module.exports = exports = function Constructor() {
    // ... code
    };
    在node的包装器函数中,我们可以看到有exports和module参数。当我们执行

    console.log(module.exports === exports);//true

结果会返回ture。可以得出其实这俩个变量是指向同一块内存地址。
无论是module.exports,或者是exports,其实都是为了调用require()的时候,处理放出来的变量。官方例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
function require(/* ... */) {
const module = { exports: {} };
((module, exports) => {
// 模块代码在这。在这个例子中,定义了一个函数。
function someFunc() {}
exports = someFunc;
// 此时,exports 不再是一个 module.exports 的快捷方式,
// 且这个模块依然导出一个空的默认对象。
module.exports = someFunc;
// 此时,该模块导出 someFunc,而不是默认对象。
})(module, module.exports);
return module.exports;
}

可以看到,exports只是module.exports的一个引用。require最后返回的module.exports,如果exports指向了其他对象,那么是无法返回对象的。例如:

1
2
3
4
5
6
7
8
9
10
11
12
module.exports.hello = true; // 从对模块的引用中导出
exports = { hello: false }; // 不导出,只在模块内有效
~~~

这里需要注意的时候,我们在导出一个变量的时候,是这样使用exports的:
>exports.fun = () => {}

这样写,exports并没有改变引用,只是增加了一个属性,也就是说exports指向的那块内存增加了一个属性。所以这样才可以导出。

# 模块无限循环引用问题
循环引用就是:
a.js

console.log(‘a 开始’);
exports.done = false;
const b = require(‘./b.js’);
console.log(‘在 a 中,b.done = %j’, b.done);
exports.done = true;
console.log(‘a 结束’);

1
b.js

console.log(‘b 开始’);
exports.done = false;
const a = require(‘./a.js’);
console.log(‘在 b 中,a.done = %j’, a.done);
exports.done = true;
console.log(‘b 结束’);

1
main.js

console.log(‘main 开始’);
const a = require(‘./a.js’);
const b = require(‘./b.js’);
console.log(‘在 main 中,a.done=%j,b.done=%j’, a.done, b.done);

1
2
3
当 main.js 加载 a.js 时,a.js 又加载 b.js。 此时,b.js 会尝试去加载 a.js。**为了防止无限的循环,会返回一个 a.js 的 exports 对象的 未完成的副本 给 b.js 模块**。 然后 b.js 完成加载,并将 exports 对象提供给 a.js 模块。

当 main.js 加载这两个模块时,它们都已经完成加载。 因此,该程序的输出会是:

$ node main.js
main 开始
a 开始
b 开始
在 b 中,a.done = false
b 结束
在 a 中,b.done = true
a 结束
在 main 中,a.done=true,b.done=true



# 模块优先级调用问题
在node中,require其它模块的时候,有好几种方式:    
1、从核心模块中引用,比如http,net,fs,dns等模块。    
2、文件模块,从文件中引用,比如require('/home/marco/foo.js')    
3、目录模块,例如 require('./some-library') 会试图加载:
* ./some-library/index.js
* ./some-library/index.node   
* 目录模块还有一种是根据package.json加载。

4、从 node_modules 目录加载   
5、从全局目录加载   
node中一共有以上5中导入模块的方法。游戏就是从1到5。第2和第3种方式都采用相对路径,可以归为一类。其他几种可以归为一类,都是都通过node本身的机制进行导入。第5种是根据环境变量NODE_PATH进行查找。
# 总结
module其实是每个模块的一个参数,不是全局变量。
# 参考
https://nodejs.org/docs/latest/api/modules.html#