本文深入探讨JavaScript模拟面向对象编程时,constructor与instanceof在原型继承中的矛盾关系。通过代码示例分析prototype赋值、对象字面量定义对constructor的影响,以及手动拷贝方法对instanceof关系的破坏,揭示两者难以兼得的底层机制。
JavaScript面向对象模拟的挑战
尝试在JavaScript中实现面向对象编程的开发者,往往投入大量精力在模拟OO机制上,而非专注于业务逻辑本身。
这种现象对于习惯Java、C++或PHP的开发者而言,是难以想象的。
更令人头疼的是,这种模拟OO的行为对高级JavaScript程序员有着一种近乎邪恶的吸引力。
因为这类工作凌驾于具体业务之上,带来一种创造新编程语言的快感,让开发者的智力得以尽情挥洒。
这正如前些年大家热衷于将网站的common.js编写成框架一样,直到YUI、jQuery等强势框架的出现才稍有平息。
然而,尽管各大框架都提供了JavaScript OO模拟方案,但尚未出现能够一统江湖的霸主。
或许江湖本就不需要霸主,又或者大家只需等待JS2.0+标准到来即可。
constructor与instanceof的和谐表象
如果仅以能否使用new关键字来衡量面向对象,那么JavaScript在这方面表现相当出色。
相关代码如下:
function Person(name){
this.name = name;
}
var lenel = new person("lenel");
alert(lenel.constructor === Person);
alert(lenel instanceof Person === true);
alert(lenel instanceof Object === true);到此为止,一切看起来都很和谐。
对象的constructor属性正如字面意义,指向构造它的Person函数。
对象是构造它的Person的一个实例(instance)。
所有对象都是Object的实例,这一点与Java非常相似。
JavaScript通过原型(prototype)机制来实现方法拓展和继承。
相关代码如下:
Person.prototype.getName = function(){
return this.name
};这样定义后,所有对象都拥有了getName方法。
当然,也可以将方法写在对象构造时。
相关代码如下:
function Person(name){
this.name = name;
this.getName = function(){
return this.name;
};
}但是这种做法不仅带来额外的性能损耗,还赋予了访问私有变量的特权。
它与使用prototype的写法还有其他不同之处,但这并非本文重点。
接下来讨论继承,常见的写法如下。
相关代码如下:
function Stuff(name,id){
this.name = name;
this.id = id;
}
Stuff.prototype = new Person();
var piupiu = new Stuff("piupiu","007");
alert(piupiu.getName());非常好,成功继承了getName方法。
原型继承中的矛盾显现
现在考察instanceof的行为。
相关代码如下:
alert(piupiu instanceof Stuff === true);
alert(piupiu instanceof Person === true);非常好,说明Stuff和Person存在关系。piupiu是它们的实例,这非常符合Java的直觉。
接下来再考察constructor属性。
相关代码如下:
alert(piupiu.constructor === Stuff);//false
test(piupiu.constructor === Person);//true问题出现了,明明是用Stuff构造的,为什么constructor却是Person?
在这里强行解释道理显得牵强,我们只能记住结论。
结论:对象的constructor属性并非指向其构造器,而是指向其构造器的prototype属性的constructor属性。
文字功底有限,读起来可能仍觉晦涩。
这里再次明确:
对象piupiu的constructor属性指向的是其构造器Stuff的prototype属性的constructor属性。
因为执行了Stuff.prototype = new Person();
所以Stuff.prototype.constructor === Person。
因此piupiu.constructor === Person。
对象定义与constructor的牺牲
上述结论不仅在对象继承时出现,在定义对象时同样存在。
相关代码如下:
function Student(name){
this.name = name;
}
Student.prototype = {
getName:function(){
return this.name;
},
setName:function(name){
this.name = name;
}
}如果方法较多,常采用这种写法,看起来更规范。
其实这种写法同样牺牲了constructor属性。
相关代码如下:
var moen = new Student("moen");
alert(moen.constructor === Student);//false
alert(moen.constructor === Object);//true因为在{}相当于new Object(),所以根据前述结论,当prototype={}时,constructor发生了改变。
维护constructor导致的父子关系丢失
为了保卫constructor,在继承时我们常用循环将父类方法拷贝到子类。
相关代码如下:
function Stuff1(name,id){
this.name = name;
this.id = id;
}
for(var fn in Person.prototype){
Stuff1.prototype[fn] = Person.prototype[fn];
}
var piupiu1 = new Stuff1("piupiu1","008");
alert(piupiu1.getName() === "piupiu1");
alert(piupiu1.constructor === Stuff1);It works!当我们兴冲冲地把父类方法都继承下来时,却丢失了父子关系。
相关代码如下:
alert(piupiu1 instanceof Stuff1 === true);//true
alert(piupiu1 instanceof Person === true);//false显然,我们没有说过Stuff1是继承至Person啊,仅一个for循环能说明什么呢。
总结:框架使用与团队规范
这好像是一对矛盾,顾此必失彼。
因此,深入使用对象时,甚至一不小心,都会让instanceof和constructor失去字面含义。
因此,在使用框架时,如果提供了继承功能,不测试就不会知道为了继承放弃了哪个。
因此,模拟OO时就必须要提供替代这两个字面含义的属性或方法的实现,而且必须让使用者知晓!
模拟OO绝对不只这个问题,在子类方法中调用父类的同名方法,这个OO语言常见的特性,
JavaScript实现起来也非常困难,大师们基本都有各自的一套,只不过有些用心去做了用起来相比之下更自然些,如Base库。
当然可以摒弃instanceof和constructor的使用,就当不知道有这么一回儿事儿,活儿仍可继续,死不了人的。
但如果你不是单枪匹马作战,那就通知你的Partner也摒弃这对冤家吧。
类似今天这点事儿,老鸟们都清楚的很,但如果不是团队中每个人都有统一的认识,那就是隐患。
一个好玩儿的现象:《JavaScript高级程序设计》强调instanceof,而《精通JavaScript》强调constructor。而各自对另一个一笔带过或只字不提。怨念..甚深...