[Node.js] 模块化编程中的对象问题
按照习惯的面向对象的写法,我们很容易写出来这样的模块:
var value = -1; function set(num) { value = num; } function get() { return value; } exports.value = value; exports.set = set; exports.get = get;
名字就定为Number模块吧。
这里我们把value作为一个公开的变量值,并把get和set方法也公开。
对模块进行一些操作,却发现:
> var n = require('./Number'); undefined > n { value: -1, set: [Function: set], get: [Function: get] } > n.set(10); undefined > n.get(); 10 > n.value -1
用get和set方法对值进行修改和读取是正确的,但直接读取value却发现并没有改变,值一直是初始值。
在node的js源代码中查找无果后到node的lib下查找关联的c++代码,发现在require进行上下文切换的是这段代码:
Local<Object> sandbox; if (context_flag == newContext) { sandbox = args[sandbox_index]->IsObject() ? args[sandbox_index]->ToObject() : Object::New(); } else if (context_flag == userContext) { sandbox = args[sandbox_index]->ToObject(); }
这段代码将传进来的之前解释模块文件得到的exports出的接口转换成c++的对象一个个放入sandbox中,然后进行上下文切换。
如果这个接口在c++中判断是个js对象,则直接转换成c++对象,否则则创建一个新的c++对象存放接口。
于是,现在的问题就是,前面我们的value究竟是不是一个js对象?
> typeof n.value 'number'
用typeof查看,在js里是一个number对象,那在C++里这个是不是对象呢?IsObject()判断的是什么样的Object呢?
后来终于找到这张图和IsObject的解释:
V8EXPORT bool v8::Value::IsObject ( ) const Returns true if this value is an object.
可以看出来在V8里面,Boolean,Number和String都分别有两种不同的对象类型,分别属于V8::Object和V8::Primitive下,IsObject判断的是是否是V8::Object。
再回到前面的问题,可以推测js中的number在c++中对应的类型应该是V8::Number,而js中的Number对象在c++中对应的才是V8::NumberObject。
但是,IsObject判断的范围究竟是什么?JS的对象还是V8::Object?为了验证这个想法,再修改Number模块:
var value = new Number(-1); function set(num) { value = new Number(num); } function get() { return value.toString(); } exports.value = value; exports.set = set; exports.get = get;
把value改成Number对象,重新运行:
> var n = require('./Number'); undefined > n { value: {}, set: [Function: set], get: [Function: get] } > n.set(10); undefined > n.get(); '10' > n.value {} > n.value.toString(); '-1'
看来判断的是js中的对象,而js中number,string,boolean都算是对象,V8::Value::IsObject()判断都为true。
那究竟还有什么原因会导致文章开头这样错误的结果呢?get和set中的value会不会指向的是另一个变量,而不是模块中的局部参数value?
再次回到问题的起点,会不会是我的代码出了问题?
经过一番研究,发现的确是作为新手的我太不了解JS了。这里有两个关键的知识点:
从JavaScript内部工作原理去看,JavaScript处理上下文分为两个阶段:
1. 进入执行上下文
2. 执行代码
可以理解为,第一个阶段是静态处理阶段,第二个阶段为动态处理阶段。
而在静态处理阶段,就会创建 变量对象(variable object),并且将变量申明作为属性进行填充。
到了执行阶段,才会根据执行情况,来对变量对象中属性(就是申明的变量)的值进行更新。
JavaScript是弱类型语言,所有的对象赋值都是引用赋值。
回过头来看自己的代码,问题就出在了两个value引用的赋值上。
var value = -1; //Local变量value,值为-1 function set(num) { value = num; //Local变量重新赋值,指向新的变量对象num } function get() { return value; } exports.value = value; //引用赋值,exports.value指向一开始初始化的变量对象 exports.set = set; exports.get = get;
于是,在使用set函数时,将Local的value引用指向了新的对象,而exports.value则还是指向原来的对象。
问题找到了,那怎么解决呢?一种方法是用this关键字:
var value = -1; function set(num) { this.value = num; } function get() { return this.value; } exports.value = value; exports.set = set; exports.get = get;
确实,it works! 但是,使用this需要格外小心。
js中this是指向运行时的上下文对象,它的值取决于调用这段代码所在函数的对象,在运行时是随着闭包不断变化的。
> var n = require('./Number'); undefined > var set = n.set; undefined > n { value: -1, set: [Function: set], get: [Function: get] } > n.set(0); undefined > n { value: 0, set: [Function: set], get: [Function: get] } > set(1); undefined > n { value: 0, set: [Function: set], get: [Function: get] } > value 1
运行下可以发现,如果是直接使用set函数,this指向当前的上下文,即node的全局对象,于是this.value就变成了全局的一个value值。
使用this时,要么在外部调用时使用call或者apply指定函数运行的上下文,要么使用一个固定的上下文对象。
要更深入理解this,需要理解js中的闭包,可以参考:
http://blog.goddyzhao.me/post/11311499651/closures
另外一种解决方案就是直接对exports.value赋值,我想这应该是最终的解决方案。
var value = -1; function set(num) { exports.value = num; } function get() { return exports.value; } exports.value = value; exports.set = set; exports.get = get;
> var n = require('./Number'); undefined > n { value: -1, set: [Function: set], get: [Function: get] } > n.set(0); undefined > n { value: 0, set: [Function: set], get: [Function: get] } > var set = n.set; undefined > set(1); undefined > n { value: 1, set: [Function: set], get: [Function: get] }
参考资料:
http://bespin.cz/~ondras/html/classv8_1_1Value.html
http://cnodejs.org/topic/4f16442ccae1f4aa270010c1
http://howtonode.org/what-is-this
https://github.com/joyent/node/blob/master/src/node_script.cc