JavaScript的继承非常有研究价值,我最近也是在这上面花了很多时间,对我理解OOP起到了很大的作用。
基于原型链
很自然的,既然对象可以指向一个原型类,并且当对象查找属性时会顺着原型链而上,那么原型完全可以作为JavaScript解决继承问题的方法。
这也是JavaScript继承方式最大的特色。
function Person() {
this.name = "Jaycee";
this.age = 19;
}
Person.prototype.sayName = function () {
return console.log(this.name);
};
function Student() {
this.number = 31801319;
}
Student.prototype = new Person();
Student.prototype.sayNumber = function () {
return console.log(this.number);
};
var student1 = new Student();
student1.sayName();
student1.sayNumber();
上面的代码用Student
去继承Person
,我们仅仅把构造函数和原型混成模式
改了一下就变成了原型继承,是的,可以说两者简直一摸一样。
如果你还不清楚这些设计模式,可以参考我前几天写的:
使用设计模式创建JavaScript对象
那么既然如此,那么这种创建方式的弊端也很明显了。
- 不能使用对象字面量快速地创建原型,因为这样做会重写原型链。
- 当一个对象被多个子类继承的时候,子类共享了它。
- 创建子类型的时候,不能向超类的构造函数传递参数。
还有一种基于原型的继承写法,由道格拉斯·克洛克福德提出的原型式继承。
这个方法不需要我们自己创建构造函数,听起来可能有点神奇。
ES5还为这个方法添加了新的方法Object.create()
,克洛克福德的原方案是直接使用object()
。
var Person = {
name: "Jaycee",
age: 19
};
var student1 = Object.create(Person, {
number: {
value: 31801319
}
});
student1.sayName = function () {
console.log(this.name);
};
student1.sayNumber = function () {
console.log(this.number);
};
student1.sayName();
student1.sayNumber();
把Person
对象传入object
,这样子,object
就会以Person
作为原型,自动创建一个新的对象。
这个方法和上一个方法的缺点是一样的。
借用构造函数
这种想法非常自然,也是其他很多语言的做法。
也就是,在子类型构造函数的内部调用超类型的构造函数。
别忘了,apply()
和call()
函数。
function Marker() {
this.colors = ["red", "blue", "green"];
}
function Canvas() {
Marker.call(this);
}
var canvas1 = new Canvas();
canvas1.colors.push("black");
console.log(canvas1.colors); //[ 'red', 'blue', 'green', 'black' ]
var canvas2 = new Canvas();
console.log(canvas2.colors); //[ 'red', 'blue', 'green' ]
canvas1
和canvas2
之间不再共享属性了,这很好。
但这个方法,产生了很多可以被重用的代码,所以我们不妨和原型链结合来做继承。
组合继承
function Marker(brand) {
this.brand = brand;
this.colors = ["red", "blue", "green"];
}
Marker.prototype.getBrand = function () {
return this.brand;
};
function Canvas(brand, owner) {
Marker.call(this, brand);
this.owner = owner;
}
Canvas.prototype = new Marker();
Canvas.prototype.getOwner = function () {
return this.owner;
};
var canvas1 = new Canvas("Muji", "Jaycee");
console.log(canvas1.getBrand()); //Muji
console.log(canvas1.getOwner()); //Jaycee
组合继承有一个问题,它调用了两次超类的构造函数。
第一次在例子的第14行,第二次在例子的第10行,怎么回事?
第一次调用是创建一个对象来作为Canvas
的原型,这可以理解。
那么第二次在干什么?
第二次事实上是重写了这个对象的一些方法,注意,借用构造函数它把Marker创建的属性和方法添加到了新的Canvas
对象中,而不是原型,这样就屏蔽了原型中的属性和方法。
寄生式继承
这种方式的思路与寄生构造函数与工厂模式类似,也就是把构造函数仅仅作为用于封装过程的函数,在函数内部增强对象,也就是添加方法和属性。
function createAnother(original) {
var clone = object(original);
clone.sayHi = function () {
console.log("hi");
};
return clone;
}
但这样的一个问题就是无法实现共用。
没关系,我们还可以组合使用其他继承方法来达到这个目的。
接下来要介绍的寄生组合式继承解决了前面在组合继承遇到的一些问题。
我们改进一下前面组合继承的代码。
function Marker(brand) {
this.brand = brand;
this.colors = ["red", "blue", "green"];
}
Marker.prototype.getBrand = function () {
return this.brand;
};
function Canvas(brand, owner) {
Marker.call(this, brand);
this.owner = owner;
}
function inheritPrototype(subType, superType){
var prototype = Object(superType.prototype);
prototype.constructor = subType;
subType.prototype = prototype;
}
inheritPrototype(Canvas, Marker);
Canvas.prototype.getOwner = function(){
return this.owner;
};
var canvas1 = new Canvas("Muji", "Jaycee");
console.log(canvas1.getBrand()); //Muji
console.log(canvas1.getOwner()); //Jaycee
这个方法的巧妙在于,我们把Marker
的原型作为了Canvas
的原型,然后在借用构造函数的时候调用Marker
的构造函数。这样我们还是初始化了两次,但把第一次初始化变成了初始化Marker
的原型。
但是要注意,为了弥补重写原型造成constructor
缺失,我们需要重新设置一下constructor
。