前后端分离开发时,难免遇到客户端与服务端进度不一致。如果需要提前获取数据并测试数据请求功能的话,可以使用 Node.js 开发一个简单的数据服务器,通过 Node.js 分析请求,经过路由(使用 EventEmitter 实现)后让相应的处理函数返回提前写好的静态数据。
Node.js(以下简称 node)中的文件即为一个模块,本文考虑的是自定义的 js 模块。在 node 环境下,每个 js 文件都会被解析为 Module 类的对象。
自定义 js 模块使用 exports 或 module.exports 封装函数或对象,在其它 js 文件中使用 require 引入模块。但在 js 文件中,require, exports 和 module 这3个变量并没有定义,也并非是全局函数/对象。
查看 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 文件封装为一个函数对象,而之前提到的三个变量都是作为函数的参数,供给内部使用的。
在一个空白的 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();
可以看到,程序首先执行 Hello()
输出了 hello。
然后使用 new 实例化函数对象 Hello,实例化的过程中,执行了 console.log('here')
输出了第二个 hello,并返回了一个实例对象,这个对象的构造器为 Hello,包含 setName,sayHello 方法。
使用这个对象变量 h
对函数对象 Hello 中定义的属性和方法进行调用是完全正确的!
回到之前的讨论,在模块中封装 Hello 函数对象后,在其它文件使用 require 引入的模块其实也是一个 Hello 函数对象,需要实例化才能调用其中定义的方法。
其实到这里已经可以明白了,“封装方法”与“封装对象”本质上是一个是否改变 module.exports 类型的问题。
“封装方法”时,只是给 module.exports 对象添加字段,其它文件引入模块,获取的是一个普通对象,所谓调用方法,其实就是直接调用对象内部字段指向的函数块而已。
“封装对象”时,如果是将函数对象直接赋给 module.exports,这时 module.exports 类型变为函数对象(构造器),函数对象本身是可执行的,外部通过它的名字索引函数块位置,传入参数来执行函数体,但这个时候,外界无法获取函数对象内部定义的属性或方法。
但是与函数对象性质相同,只要将模块实例化(对应函数对象的实例化),得到的实例是以函数对象为构造器生成的一个普通对象,这个对象包含了函数对象中定义的变量和方法。
个人总结,如果差错,敬请指正