通过挟持 this 指针在 JavaScript 中模拟 private
2011-11-06 by Dron
JavaScript 是一种解释型的、基于对象的脚本语言,没有严格意义上的类,在这一点上不同于 C++、Smalltalk 或者 Java,不过作为替代,它支持构造函数(constructors),可通过执行代码创建对象:给对象分配存储,然后通过赋初始值来初始化对象属性的全部或部分。构造函数有个指向其原型对象的属性叫做 prototype,利用 prototype 可模拟出一个基本可用的“类”来。
类有封装、继承和多态等特征,其中继承和多态不是本文关注的,本文侧重于探讨类封装中关于私有成员(private)的实现。
JavaScript 中,可通过 new 运算符来创建一个“类”的实例对象,然而,该实例对象与一个直接量的对象无异,其成员(属性和方法)都是直接对外暴露的,无法像其它高级语言中用 private 声明保护起来。幸好,我们可以利用变量的作用域来变通解决这个问题,达到模拟 private 的目的。
现在要做的是,往构造函数和“类”方法中的 this 引用上增加一些额外的成员(属性和方法),并且这些成员不能被外部访问到,唯一的办法就是把 this 指针和实例对象拆分开,使 this 指针不指向实例对象。但是这样做还不够,我们还得保证一些对外公开的成员不管在内部或者外部都能正常访问,这就要使被拆分开的 this 指针和实例对象存在一些同名成员指向相同的引用。
我写了一段简单的示例代码,通过 createClass 函数来创建一个“类”,在 createClass 中利用 function 的 apply 方法来修改构造函数和“类”方法成员中的 this 指针,使之指向 privates,由于每次 createClass 运行时都会分配一个新的变量作用域,这个作用域的范围局限于 createClass 函数内部,正好用来保护 privates 成员不被外部访问到。同时,我把 publics 和 privates 做了一些揉合,它们之间存在一些同名的成员,这些成员被挂到了 prototype 上,使内外部都能正常使用。代码如下:
function createClass(conf){
var fn, prototype, privates;
publics = conf.publics;
privates = conf.privates || new Object();
fn = function(fn){
return function(){
return fn.apply(privates, arguments);
};
}(conf.constructor || new Function());
prototype = fn.prototype;
for(var publicName in publics){
if(!publics.hasOwnProperty(publicName))
continue;
if(typeof publics[publicName] == "function")
prototype[publicName] = function(publicName){
return function(){
return publics[publicName].apply(privates, arguments);
}
}(publicName);
else prototype[publicName] = publics[publicName];
if(!privates[publicName])
privates[publicName] = prototype[publicName];
}
return fn;
}
下面这段代码可用来测试:
var klass = createClass({
constructor: function(){
console.log(this.message); // hello
},
publics: {
message: "world",
message2: "javascript",
sayHello: function(){
return this.message;
},
sayJavaScript: function(){
return this.message2;
},
sayYouCantSeeMe: function(){
return this.message3;
},
sayInteresting: function(msg){
return this.interesting();
}
},
privates: {
message: "hello",
message3: "you cant see me",
interesting: function(){
return "interesting";
}
}
});
var instance = new klass();
// case0 在 constructor 里直接访问 message,得到的是 privates 里的 message 值
// case1
// message 同时存在于 publics 和 privates 中,外部访问得到 world
// 而内部访问得到 hello
console.log("instance.message => ", instance.message);
console.log("instance.sayHello() => ", instance.sayHello());
// case2
// message2 只在 publics 中,不管是外部或内部访问,都得到 javascript
console.log("instance.message2 => ", instance.message2);
console.log("instance.sayJavaScript() => ", instance.sayJavaScript());
// case3
// message3 被定义为私有属性,外部访问不到,得到 undefined
// 而内部可以正常访问,得到 you cant see me
console.log("instance.message3 => ", instance.message3);
console.log("instance.sayYouCantSeeMe() => ", instance.sayYouCantSeeMe());
// case4
// 同样的,interesting 是私有方法,只有内部才能访问,这里得到 undefined
// 这里得到 interesting
console.log("instance.interesting => ", instance.interesting);
console.log("instance.sayInteresting() => ", instance.sayInteresting());
完整的示例见这里,当然,由于 this 已经不指向实例对象,this 的成员被修改,即使同名的实例对象的成员也得不到同步,这样将导致实例的属性用起来比较费劲,对于属性的对外访问,建议分别做成 getXXX 和 setXXX 两个方法。
以上 private 的模拟,在 JavaScript 中的应用还是少数,仅供探讨,如要应用到项目中,请慎重。