在pseudo-classical模式中,对象是通过构造函数创建的,并且它的方法是直接被放到prototype中的。
pseudo-classical模式也应用在一些框架中,例如Google Closure Library, Native JavaScript objects等。
Pseudo-class 声明
“pseudo-class”这个词其实不是很准确,因为与PHP, Java,C++等语言相比,javaScript事实上没有类,但是pseudo-class模式在某种程度上是与它们相近的。
这篇文章假设你已经对原型继承的机制很了解了,如果不了解请查看原型继承这篇文章。
一个pseudo-class由构造函数和方法组成。例如下面的 Animal pseudo-class,有一个sit方法和两个属性。
function Animal(name){
this.name = name;
}
Animal.prototype = {
canWalk: true,
sit: function(){
this.canWalk = false;
alert(this.name + ' sits down.');
} }
var animal = new Animal('Pet');
alert(animal.canWalk); //true animal.sit(); //pet sits down alert(animal.canWalk); //false
1. 当new Animal(name) 被调用时,生成新对象的__proto__指向Animal.prototype,请看下面图片的左边部分。
2. animal.sit方法改变了实例中的animal.canWalk,因此该animal对象不能走,而其他对象仍可以走。
总结pseudo-class:
(1)方法和默认的属性是在原型中的。
(2)在prototype中的方法使用的this, 指的是当前对象,因为this的值仅仅依赖调用的上下文,因此animal.sit()将this设为animal。
在以上的两点中隐藏着一些使用的危险,请看下面的例子。
你是仓鼠农场的老大,让你下面的程序员接受一个任务去创建Hamster构造函数和原型。
Hamster应该有一个food数组来存储和found方法来操作food数组,这两个方法都被添加到了原型中。
你的程序员给你带来了以下的解决方案。代码看起来还挺好的,当你创建两个Hamster时,本来饲料是属于其中一个的,但现在两个都有了。
那该怎么解决这种问题呢?
function Hamster(){};
Hamster.prototype = {
food: [],
found: function(something){
this.food.push(something);
} }
//创建一个懒惰的hamster,和一个勤快的hamster,勤快的有饲料
speedy = new Hamster();
lazy = new Hamster();
speedy.found("apple");
speedy.found("orange");
alert(speedy.food.length);
//2alert(lazy.food.length);
//2
解决方法:
让我们仔细分析下,speedy.found(“apple”)执行时发生了什么事?
(1)解释器查找在speedy中查找found,但是speedy是一个空对象,因此会失败。
(2)解释器到speedy.__proto__(==Hamster.prototype)查找,然后找到了found方法后执行。
(3)在先前的执行阶段,由于调用speedy.__proto__,this是被设置成speedy对象。
(4)this.food在speedy中没有被找到,但是可以在speedy.__proto__中被找到。
(5)”apple”被加到了speedy.__proto__.food中。
在__proto__中的food被改变时,却在两个hamster对象中共享了,必须要解决这个问题。
解决这个问题
为了解决这个问题,我们要确定每个hamster都有他们自己的食物,可以通过在构造函数中赋值做到。
function Hamster(){
this.food = [],};
Hamster.prototype = {
found: function(something){
this.food.push(something);
} }
speedy = new Hamster();
lazy = new Hamster();
speedy.found("apple");
speedy.found("orange");
alert(speedy.food.length); //2alert(lazy.food.length); //0
继承
让我们创建一个新的从animal继承的一个Rabbit类。
function Rabbit(name){
this.name = name;
}
Rabbit.prototype.jump = function(){
this.canWalk = true;
alert(this.name + ' jumps!');
}
var rabbit = new Rabbit('John');
正如你所看到的,结构与Animal很相似。
为了从Animal中继承,我们需要Rabbit.prototype.__proto__ == Animal.prototype. 这是一个自然的想法,因为如果一个方法不能在Rabbit.prototype中找到,我们可以在Animal.prototype中去寻找。具体看下图说明。
为了实现这条链,我们需要创建一个从Animal.prototype继承来的Rabbit.prototype空对象,然后再向其中添加方法。
function Rabbit(name){
this.name = name; }
Rabbit.prototype = inherit(Animal.prototype);
Rabbit.prototype.jump = function(){...}
在上面的代码中,inherit是一个通过给定的__proto__属性来创建空对象的方法。
function inherit(proto){
function F(){};
F.prototype = proto;
return new F; }
最后两个对象完整的代码如下所示。
function Animal(name){
this.name = name;}
Animal.prototype = {
canWalk: true,
sit: function(){
this.canWalk = false;
alert(this.name + ' sits down.');
} }
function Rabbit(name){
this.name = name; }
//继承
Rabbit.prototype = inherit(Animal.prototype); //Rabbit方法Rabbit.prototype.jump = function(){
this.canWalk = true;
alert(this.name + ' jumps!'); }
var rabbit = new Rabbit('Sniffer');
rabbit.sit(); //Sniffer sitsrabbit.jump() //sniffer jumps!
function inherit(proto){
function F(){};
F.prototype = proto;
return new F;
}
不要通过new Animal()来继承。这种方法应用得非常多,但是通过Rabbit.prototype = new Animal()是一种错误的继承方式。因为new Animal()这种方式并没有传递参数name,构造函数也许严格限制需要传递参数才行,所以这种情况下使用这种继承方式是不行的。
事实上,我们只想从Animal中继承,而不是去创建一个Animal实例吧?这才符合继承的实质。所以Rabbit.prototype = inherit(Animal.prototype)是更好点。
调用父类的构造函数
”superclass”构造函数不会被自动调用,我们可以通过apply来实现。
function Rabbit(name){
Animal.apply(this,arguments);
}
在当前对象的上下文执行Animal构造函数,从而达到改变name值的目的。
重写方法(多态)
为了重写父类方法,在子类的原型中代替它。
Rabbit.prototype.sit = function(){
alert(this.name + ' sits in a rabbity way.');
}
当调用rabbit.sit()时,搜索链为:rabbit -> Rabbit.prototype -> Animal.prototype,如果在Rabbit.prototype中找到,就会停止去Animal.prototype寻找。
当然我们也可以直接在对象中重写方法。
rabbit.sit = function(){
alert('a special sit of this very rabbit '+ this.name);
}
重写后再调用父类的方法
当一个方法被重写时,我们还想调用之前老的方法,如果可能我们可以直接请求父类的prototype来实现。
Rabbit.prototype.sit = function() {
alert('calling superclass sit:')
Animal.prototype.sit.apply(this, arguments)
}
所有父类方法通过apply/call方法传递当前对象this来调用,Animal.prototype.sit()调用将 Animal.prototype作为this。
去除对父类的直接引用
在上面的例子当中,我们可以通过Animal.apply...
或者Animal.prototype.sit.apply....
直接调用父类。
正常情况下,我们不应该那样做。以后重构代码也许会改变父类的名字,或者在层级中引入新的类。
一般来说,程序语言允许调用父类方法通过使用特别的关键字,如parent.method()或者
super()。
javaScript没有这样的特性,但我们可以模拟它。
下面的extend方法可以实现继承,却不需要直接指向父类的引用。
function extend(child, parent) {
child.prototype = inherit(parent.prototype);
child.prototype.constructor = child;
child.parent = parent.prototype;
}
用法:
function Rabbit(name){
Rabbit.parent.constructor.apply(this.arguments); //调用父类构造器} extend(Rabbit,Animal);
Rabbit.prototype.run = function(){ Rabbit.parent.run.apply(this,arguments); //调用父类方法
}
事实上现在我们可以重命名Animal或者创建一个中间类
GrassEatingAnimal
,但是实际情况的改变只会涉及到Animal和extend(...)。
private或protected方法(封装)
可保护的方法和属性通过命名的习惯来支持。因此,一个以下划线开始的方法不应该被外面直接调用。
private方法经常不支持。