Node.js 自定义模块封装及其底层原理

前言

前后端分离开发时,难免遇到客户端与服务端进度不一致。如果需要提前获取数据并测试数据请求功能的话,可以使用 Node.js 开发一个简单的数据服务器,通过 Node.js 分析请求,经过路由(使用 EventEmitter 实现)后让相应的处理函数返回提前写好的静态数据。

什么是 Node.js 模块系统

Node.js(以下简称 node)中的文件即为一个模块,本文考虑的是自定义的 js 模块。在 node 环境下,每个 js 文件都会被解析为 Module 类的对象。

自定义 js 模块使用 exports 或 module.exports 封装函数或对象,在其它 js 文件中使用 require 引入模块。但在 js 文件中,require, exports 和 module 这3个变量并没有定义,也并非是全局函数/对象。

module 变量的来源

查看 node 源码(module.js),可以看到在编译 js 文件的时候 node 对 js 文件内容进行了头尾的包装:

NativeModule.wrap = function(script) {		// script 为 js 文件的全部内容
  return NativeModule.wrapper[0] + script + NativeModule.wrapper[1];
};

NativeModule.wrapper = [
  '(function (exports, require, module, __filename, __dirname) { ' ,
  '\n});'
];

可以看到,node 在 js 文件头部加了(function (exports, require, module, __filename, __dirname) {,在尾部加了\n});。这样相当于把 js 文件封装为一个函数对象,而之前提到的三个变量都是作为函数的参数,供给内部使用的。

module 变量的构造

在一个空白的 js 文件中,使用 console.log(module) 输出 module 变量:
在这里插入图片描述

可以看到 module 对象为 Module 类的实例,其内部定义了一个 exports 对象,也就作为模块对外开放的接口。

而在 js 文件中使用的 exports 变量其实是 modules.exports 的一个引用,我们使用 exports.XXX 封装函数对象会同步到 modules.exports。
但是,把一个(函数)对象赋值给 modules.exports 时,会破坏 exports 对其的引用关系!此后,exports 与 modules.exports 再无瓜葛。

function Hello() { 
    var name; 
    this.setName = function(thyName) { 
        name = thyName; 
    }; 
    this.sayHello = function() { 
        console.log('Hello ' + name); 
    }; 
}; 

module.exports = Hello;

exports.sad = function () {
    console.log('sad');
}

console.log(module.exports);
console.log(exports);

上面的代码,将函数对象 Hello 赋给 module.exports,之后又试图用 exports 封装了一个名为 sad 的匿名函数对象,控制台输出两位:
在这里插入图片描述
可以看到,Hello 成功占据了 modules.exports,而 exports 自成一家,其实是因为它还追随着 modules.exports 旧的内存空间,虽然 modules.exports 早就离它而去了。

两种自定义模块封装方式

封装方法

我们知道 modules.exports 是一个 JS 对象,我们可以直接为其添加方法,在其它文件使用 require 获取模块时,直接获取到 modules.exports 对象,便可以进行方法的调用。
举个简单的例子:

// hello.js
exports.world = function() {
    console.log('Hello World')
}
exports.city = function() {
    console.log('Hello City');
}
console.log(module.exports);

在 main.js 引入模块:

// main.js
var hello = require('./hello')

hello.world();
hello.city();

控制台输出:
在这里插入图片描述
可以看到,直接使用模块对封装的函数进行调用是合法的。

封装对象

如果我们创建一个函数对象,并将其赋给 module.exports,会发生什么呢?

// hello_.js
function Hello() { 
    var name; 
    this.setName = function(thyName) { 
        name = thyName; 
    }; 
    this.sayHello = function() { 
        console.log('Hello ' + name); 
    }; 
    console.log('here');
}; 

module.exports = Hello;

console.log(module.exports);

在 main.js 引入该模块,并直接尝试直接执行 & 调用 sayHello 方法:

// main.js
var hello_ = require('./hello_');

hello_();
hello_.sayHello();

控制台报错:
在这里插入图片描述
可以看到,直接执行 hello_() 是可以的,这个操作会执行 hello.js 中定义的 Hello 函数对象中所有的可直接执行的语句(如:console.log('here');),但其内部定义的方法并不会执行。
当试图调用 sayHello 方法时就报错了,这是因为 sayHello 方法是在 Function 对象的属性中,在直接执行 Function 对象时,只会执行其中的程序体,获取返回值等,而不会处理使用 this. 定义的属性或方法。

跳出复杂的模块系统,新创建一个 js 文件来看看发生了什么:

function Hello() { 
    var name; 
    this.setName = function(thyName) { 
        name = thyName; 
    }; 
    this.sayHello = function() { 
        console.log('Hello ' + name); 
    }; 
    console.log('here')
}; 

console.log(Hello);
Hello();

var h = new Hello();
console.log(h);
h.setName('Tommy_s00');
h.sayHello();

用 node 运行它:
在这里插入图片描述

可以看到,程序首先执行 Hello() 输出了 hello。
然后使用 new 实例化函数对象 Hello,实例化的过程中,执行了 console.log('here') 输出了第二个 hello,并返回了一个实例对象,这个对象的构造器为 Hello,包含 setName,sayHello 方法。
使用这个对象变量 h 对函数对象 Hello 中定义的属性和方法进行调用是完全正确的!

回到之前的讨论,在模块中封装 Hello 函数对象后,在其它文件使用 require 引入的模块其实也是一个 Hello 函数对象,需要实例化才能调用其中定义的方法。

原理分析

其实到这里已经可以明白了,“封装方法”与“封装对象”本质上是一个是否改变 module.exports 类型的问题。

“封装方法”时,只是给 module.exports 对象添加字段,其它文件引入模块,获取的是一个普通对象,所谓调用方法,其实就是直接调用对象内部字段指向的函数块而已。

“封装对象”时,如果是将函数对象直接赋给 module.exports,这时 module.exports 类型变为函数对象(构造器),函数对象本身是可执行的,外部通过它的名字索引函数块位置,传入参数来执行函数体,但这个时候,外界无法获取函数对象内部定义的属性或方法。
但是与函数对象性质相同,只要将模块实例化(对应函数对象的实例化),得到的实例是以函数对象为构造器生成的一个普通对象,这个对象包含了函数对象中定义的变量和方法。

参考

  1. Node.js 模块机制的底层原理
  2. es6 中 class 与 function 的区别

个人总结,如果差错,敬请指正

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值