nodejs入门之模块

  • nodejs模块语法与开闭原则
  • nodejs模块的底层实现

 1、nodejs模块语法与开闭原则

关于nodejs模块我在以前的两篇博客中都有涉及,但都没有对nodejs模块的底层作作任何探讨,可是为了使相关内容跟方便查看比对理解,这里仍是先引入一下以前两篇博客的链接:html

js模块化入门与commonjs解析与应用node

ES6入门十二:Module(模块化)模块化

1.1 exports、module.exports、require()实现模块导出导入:函数

 1 //示例一:导出原始值数据
 2 //a.js--用于导出数据
 3 let a = 123;
 4 module.exports.a=a;
 5 //inde.js--用于导入a模块的数据
 6 let aModule = require('./a.js');
 7 console.log(aModule.a); //123
 8 
 9 //示例二:导出引用值数据
10 //a.js--同上
11 function foo(val){ 
12     console.log(val);
13 }
14 module.exports.foo = foo;
15 //index.js--同上
16 let aModule = require('./a.js');
17 let str = "this is 'index' module"
18 aModule.foo(str); //this is 'index' module
19 
20 //示例三:导出混合数据
21 a.js--同上
22 let a = 123;
23 function foo(val){ 
24     console.log(val);
25 }
26 module.exports = {
27     a:a,
28     foo:foo
29 }
30 //inde.js--同上
31 let aModule = require('./a.js');
32 let str = "this is 'index' module"
33 console.log(aModule.a);//123
34 aModule.foo(str); //this is 'index' module

在上面这些示例中,没有演示exports的导出,暂时能够把它看做与同等于module.exports,例如:测试

 1 //a.js -- 导出模块
 2 let a = 123;
 3 function foo(val){ 
 4     console.log(val);
 5 }
 6 exports.a = a;
 7 exports.foo = foo;
 8 
 9 //inde.js -- 引用模块a
10 let aModule = require('./a.js');
11 let str = "this is 'index' module"
12 console.log(aModule.a);//123
13 aModule.foo(str); //this is 'index' module

可是使用exports导出模块不能这么写:ui

 1 //a.js
 2 let a = 123;
 3 function foo(val){ 
 4     console.log(val);
 5 }
 6 exports = {
 7     a:a,
 8     foo:foo
 9 }
10 
11 //index.js
12 let aModule = require('./a.js');
13 let str = "this is 'index' module"
14 console.log(aModule);// {} -- 一个空对象

至于为何不能这么写,暂时不在这里阐述,下一节关于nodejs模块底层实现会具体的分析介绍,这里先来介绍nodejs模块的一个设计思想。this

1.2 nodejs模块的开闭原则设计实现编码

1 //a.js -- 导出模块
2 let num = 123;
3 let str = "this is module 'a'";
4 exports.a = a;
5 
6 //index.js -- 引用模块a
7 let aModule = require('./a.js');
8 console.log(aModule.num);//123
9 console.log(aModule.str);//undefined

这里你会发现只有被exports执行了导出的num成员才能被正常导出,而str成员没有被执行导出,在依赖a.js模块的index.js中是不能引用到a.js模块中的str成员。可能你会说这不是很正常吗?都没有导出怎么引用呢?spa

不错,这是一个很是正常状况,由于语法就告诉了咱们,要想引用一个模块的成员就必须先在被引用的模块中导出该成员。然而这里要讨论的固然不会是导出与引用这个问题,而是模块给我实现了一个很是友好的设计,假设我如今在a.js中有成员str,在index.js模块中也有成员str,这回冲突吗?显然是不会的,即便在a.js中导出str而且在index.js中引用a.js模块,由于index.js要使用a.js模块的成员str,须要使用接收模块变量aModule.str来使用。设计

 1 //a.js
 2 let num = 123;
 3 let str = "this is module 'a'";
 4 exports.num = num;
 5 exports.str = str;
 6 
 7 //index.js
 8 let aModule = require('./a.js');
 9 let str = "this is module 'index'"
10 console.log(aModule.num);//123
11 console.log(aModule.str);//this is module 'a'
12 console.log(str);//this is module 'index'

基于开闭原则的设计方式,封闭可让模块的内部实现隐藏起来,开放又能够友好的实现模块之间的相互依赖,这相对于以前咱们经常使用的回调函数解决方案,程序设计变得更清晰,代码复用变得更灵活,更关键的是还解决了js中一个很是棘手的问题——命名冲突问题,上面的示例就是最好的证实。这里须要抛出一个问题,看示例:

1 //下面这种写法有什么问题?
2 //a.js
3 let num = 123;
4 module.exports = num;
5 
6 //index.js
7 let aModule = require('./a.js');
8 let str = "this is module 'index'"
9 console.log(aModule);//123

这种写法不会报错,也能正常达到目前的需求,若是从能解决目前的功能需求角度来讲,它没错。可是开闭原则的重要思想就是让模块保持相对封闭,又有更好的拓展性,这样写显然不合适,好比就上面的代码写完上线之后,业务又出现了一个新的需求须要a.js模块导出一个成员str,这时候显然须要同时更改a.js模块和index.js模块,即便新需求不须要index.js来实现也是须要改的。因此维持模块的开闭原则是良好的编码风格。

 2、nodejs模块的底层实现原理

2.1 module.exports与exports的区别:

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

//而后在控制台直接执行a.js模块
node a.js

实际上它们是没有区别的,那为何在以前的exports不能直接等于一个对象,而module.exports能够呢?这关乎于js的引用值指向问题:

 

 当export被赋值一个对象时,就发生了一下变化:

这时候咱们能够肯定node不会导出exports,由于前面的示例已经说明了这一点,可是值得咱们继续思考的是,node模块是依据module.exports、exports、仍是它们指向的初始对象呢?这里你确定会说是module.exports,由于前面已经有示例是module.exports指向一个新的对象被成功导出,可是我并不以为前面那些示例能说服我,好比下面这种状况:

 1 //a.js模块
 2 let num = 123;
 3 function foo(val){
 4     console.log(val);
 5 }
 6 module.exports = {
 7     num:num
 8 }
 9 exports = {
10     foo:foo
11 }
12 //index.js模块
13 let aModule = require('./a.js');
14 console.log(aModule);//这里会打印出什么?

咱们现不测试也不猜想,先经过下面的示图来看下如今的a.js模块中module.exports、exports、以及它们两初始指向的空对象的关系图:

 

 这时候咱们来看一下index.js执行会输出什么?

{ num: 123 }

因此从这个结果能够看出,最后require()最后导入的是被引用模块的module.exports。探讨到这里的时候并无到达node模块的终点,咱们这里module.exports、exports、require()是从哪里来的?node系统内置变量?仍是别的?

2.2 node模块的底层实现原理

这部分的内容其实也没有太多能够说的,就前面提出来的问题其实有一个方式就可让你一目了然,只须要在一个js文件中编写一下代码,而后使用node执行这个js文件就能够了:

1 console.log(require);      // 一个方法
2 console.log(module);       //  一个对象
3 console.log(exports);      //  一个空对象
4 console.log(__dirname);    //  当前模块所在路径
5 console.log(__filename);   //  当前文件的路径

 这时由于node模块实际上底层是被放到一个当即执行函数内(不要在意xyz这个名称,由于我也不知道node底层到底用的什么名称),这些变量其实就是这个函数的参数,这个函数大概是一下形式:

1 function xyz(module.exports,require,module,__filename,__dirname){
2     //...
3     //  这里就是咱们在模块中写入的代码
4     //...
5     return module.exports;
6 }

经过上面的推断就能够获得下面这样的结果:

1 console.log(module.exports == arguments[0]);//true
2 console.log(require == arguments[1]);//true
3 console.log(module == arguments[2]);//true
4 console.log(__filename == arguments[3]);//true
5 console.log(__dirname == arguments[4]);//true

经过执行这段打印代码也确实能够获得这样的结果,到这里又有一个值得咱们关注的内容,就是每一个模块的module参数:

 1 console.log(module);
 2 Module {
 3     id: '.',//当前模块的id都是'.',在后面的parent和children里面的模块对象上的id就是的对应模块的filename
 4     exports: {},//这里是模块导出对象
 5     parent: null,//这里是当前模块被那些模块引用的模块对象列表,意思是当前模块做为那些模块的父级模块
 6     filename:'',//这里是当前文件路径的绝对路径
 7     loaded: false,//模块加载状态,若是在模块内部输出module对象它永远都会是false,由于只有这个模块加载完成以后才会被修改为true
 8     children: [
 9         // 这里是引用模块module对象列表,意思是当前模块做为了那些模块的子模块
10     ],
11     paths:[ 
12         // 这里是外部模块包的路径列表,从最近的路径(模块所在同级路径)到系统盘路径全部的node_modules文件夹路径
13      ] 
14     }

到这里有可能你还会问为何底层实现里面只有module.exports,没有export,这个解释起来真的费劲,下面这一行代码帮你搞定:

let exports = module.exports;

这篇博客主要介绍了node模块的内部内容,并未就node模块基于commonjs规范作任何介绍,是由于在以前的博客中已经有了很是全面的解析,详细参考博客开始时的链接,关于node模块加载相关内容也是在那篇博客。

相关文章
相关标签/搜索