- nodejs模块语法与开闭原则
- nodejs模块的底层实现
一、nodejs模块语法与开闭原则
关于nodejs模块我在之前的两篇博客中都有涉及,但都没有对nodejs模块的底层做做任何探讨,但是为了使相关内容更方便查看比对理解,这里还是先引入一下之前两篇博客的连接:
js模块化入门与commonjs解析与应用
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导出模块不能这么写:
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模块的一个设计思想。
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成员。可能你会说这不是很正常吗?都没有导出怎么引用呢?
不错,这是一个非常正常情况,因为语法就告诉了我们,要想引用一个模块的成员就必须先在被引用的模块中导出该成员。然而这里要讨论的当然不会是导出与引用这个问题,而是模块给我实现了一个非常友好的设计,假设我现在在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来实现也是需要改的。所以维持模块的开闭原则是良好的编码风格。
二、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模块加载相关内容也是在那篇博客。