Node.JS 模块语法与开闭原则
Node.JS 模块的底层实现
一, Node.JS 模块语法与开闭原则
关于 Node.JS 模块我在之前的两篇博客中都有涉及, 但都没有对 Node.JS 模块的底层做做任何探讨, 但是为了使相关内容更方便查看比对理解, 这里还是先引入一下之前两篇博客的连接:
JS 模块化入门与 commonjs 解析与应用
ES6 入门十二: Module(模块化)
1.1 exports,module.exports,require()实现模块导出导入:
- // 示例一: 导出原始值数据
- //a.JS-- 用于导出数据
- let a = 123;
- module.exports.a=a;
- //inde.JS-- 用于导入 a 模块的数据
- let aModule = require('./a.js');
- console.log(aModule.a); //123
- // 示例二: 导出引用值数据
- //a.JS-- 同上
- function foo(val){
- console.log(val);
- }
- module.exports.foo = foo;
- //index.JS-- 同上
- let aModule = require('./a.js');
- let str = "this is'index'module"
- aModule.foo(str); //this is 'index' module
- // 示例三: 导出混合数据
a.JS-- 同上
- let a = 123;
- function foo(val){
- console.log(val);
- }
- module.exports = {
- a:a,
- foo:foo
- }
- //inde.JS-- 同上
- let aModule = require('./a.js');
- let str = "this is'index'module"
- console.log(aModule.a);//123
- aModule.foo(str); //this is 'index' module
在上面这些示例中, 没有演示 exports 的导出, 暂时可以把它看作与同等于 module.exports, 例如:
- //a.JS -- 导出模块
- let a = 123;
- function foo(val){
- console.log(val);
- }
- exports.a = a;
- exports.foo = foo;
- //inde.JS -- 引用模块 a
- let aModule = require('./a.js');
- let str = "this is'index'module"
- console.log(aModule.a);//123
- aModule.foo(str); //this is 'index' module
但是使用 exports 导出模块不能这么写:
- //a.JS
- let a = 123;
- function foo(val){
- console.log(val);
- }
- exports = {
- a:a,
- foo:foo
- }
- //index.JS
- let aModule = require('./a.js');
- let str = "this is'index'module"
- console.log(aModule);// {} -- 一个空对象
至于为什么不能这么写, 暂时不在这里阐述, 下一节关于 Node.JS 模块底层实现会具体的分析介绍, 这里先来介绍 Node.JS 模块的一个设计思想.
1.2 Node.JS 模块的开闭原则设计实现
- //a.JS -- 导出模块
- let num = 123;
- let str = "this is module'a'";
- exports.a = a;
- //index.JS -- 引用模块 a
- let aModule = require('./a.js');
- console.log(aModule.num);//123
- 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 来使用.
- //a.JS
- let num = 123;
- let str = "this is module'a'";
- exports.num = num;
- exports.str = str;
- //index.JS
- let aModule = require('./a.js');
- let str = "this is module'index'"
- console.log(aModule.num);//123
- console.log(aModule.str);//this is module 'a'
- console.log(str);//this is module 'index'
基于开闭原则的设计方式, 封闭可以让模块的内部实现隐藏起来, 开放又可以友好的实现模块之间的相互依赖, 这相对于之前我们常用的回调函数解决方案, 程序设计变得更清晰, 代码复用变得更灵活, 更关键的是还解决了 JS 中一个非常棘手的问题 -- 命名冲突问题, 上面的示例就是最好的证明. 这里需要抛出一个问题, 看示例:
- // 下面这种写法有什么问题?
- //a.JS
- let num = 123;
- module.exports = num;
- //index.JS
- let aModule = require('./a.js');
- let str = "this is module'index'"
- console.log(aModule);//123
这种写法不会报错, 也能正常达到目前的需求, 如果从能解决目前的功能需求角度来说, 它没错. 但是开闭原则的重要思想就是让模块保持相对封闭, 又有更好的拓展性, 这样写显然不合适, 比如就上面的代码写完上线以后, 业务又出现了一个新的需求需要 a.JS 模块导出一个成员 str, 这时候显然需要同时更改 a.JS 模块和 index.JS 模块, 即使新需求不需要 index.JS 来实现也是需要改的. 所以维持模块的开闭原则是良好的编码风格.
二, Node.JS 模块的底层实现原理
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 指向一个新的对象被成功导出, 但是我并不觉得前面那些示例能说服我, 比如下面这种情况:
- //a.JS 模块
- let num = 123;
- function foo(val){
- console.log(val);
- }
- module.exports = {
- num:num
- }
- exports = {
- foo:foo
- }
- //index.JS 模块
- let aModule = require('./a.js');
- 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 文件就可以了:
- console.log(require); // 一个方法
- console.log(module); // 一个对象
- console.log(exports); // 一个空对象
- console.log(__dirname); // 当前模块所在路径
- console.log(__filename); // 当前文件的路径
这时因为 node 模块实际上底层是被放到一个立即执行函数内(不要在乎 xyz 这个名称, 因为我也不知道 node 底层到底用的什么名称), 这些变量其实就是这个函数的参数, 这个函数大概是一下形式:
- function xyz(module.exports,require,module,__filename,__dirname){
- //...
- // 这里就是我们在模块中写入的代码
- //...
- return module.exports;
- }
通过上面的推断就可以得到下面这样的结果:
- console.log(module.exports == arguments[0]);//true
- console.log(require == arguments[1]);//true
- console.log(module == arguments[2]);//true
- console.log(__filename == arguments[3]);//true
- console.log(__dirname == arguments[4]);//true
通过执行这段打印代码也确实可以得到这样的结果, 到这里又有一个值得我们关注的内容, 就是每个模块的 module 参数:
- console.log(module);
- Module {
- id: '.',// 当前模块的 id 都是'.', 在后面的 parent 和 children 里面的模块对象上的 id 就是的对应模块的 filename
- exports: {},// 这里是模块导出对象
- parent: null,// 这里是当前模块被那些模块引用的模块对象列表, 意思是当前模块作为那些模块的父级模块
- filename:'',// 这里是当前文件路径的绝对路径
- loaded: false,// 模块加载状态, 如果在模块内部输出 module 对象它永远都会是 false, 因为只有这个模块加载完成之后才会被修改成 true
- children: [
- // 这里是引用模块 module 对象列表, 意思是当前模块作为了那些模块的子模块
- ],
- paths:[
- // 这里是外部模块包的路径列表, 从最近的路径 (模块所在同级路径) 到系统盘路径所有的 node_modules 文件夹路径
- ]
- }
到这里有可能你还会问为什么底层实现里面只有 module.exports, 没有 export, 这个解释起来真的费劲, 下面这一行代码帮你搞定:
let exports = module.exports;
这篇博客主要介绍了 node 模块的内部内容, 并未就 node 模块基于 commonjs 规范做任何介绍, 是因为在之前的博客中已经有了非常全面的解析, 详细参考博客开始时的连接, 关于 node 模块加载相关内容也是在那篇博客.
来源: https://www.cnblogs.com/ZheOneAndOnly/p/11902125.html