我们知道面向对象的语言有四大基本特性:抽象、继承、封装和多态。抽象性是指将具有一致的数据结构(属性)和行为(操作)的对象抽象成类。一个类就是这样一种抽象,它反映了与应用有关的重要性质,而忽略其他一些无关内容。任何类的划分都是主观的,但必须与具体的应用有关。在C#等一些面向对象语言中有抽象类的概念,继承了抽象类的类必须实现抽象类中的方法,而在JS中由于不存在类的概念,不存在重写等关键字,所以JS中是没有抽象概念。而JS是弱类型的语言,任何一个类型都可以赋值给任一属性,所以我们可以说JS是支持多态的。所以总的来说JS支持继承,封装和多态。前面讲到了对象的创建,可以理解为封装,这次我们主要说说这个继承的思想。
与其他面向对象语言不同,JS中的继承也有着自己独特的实现方式。基本上有以下6中实现方式:原型链,借用构造函数,组合继承,原型式继承,寄生式继承,寄生组合式继承。
1、原型链继承
所谓的原型链继承即是通过将父对象直接赋值给子对象的原型,这一样子对象可以通过原型访问父对象中的属性和方法。我们知道,在创建一个新的对象时,原型指针都会默认的指向一个Object对象,因此我们可以通过原型指针访问object中的方法,比如toString方法,我们可以说该对象实际上是通过原型链继承了Object对象。实现原型链的继承方式如以下所示:
function SuperType(){
this.name = "I'm super type";
}
SuperType.prototype.sayHi = function(first_argument) {
alert("Hi");
};
function SubType(){
}
SubType.prototype = new SuperType();
var sub = new SubType();
sub.sayHi();//Hi
alert(sub.name);//I'm super type
alert(sub.constructor == SuperType);//true
通过上面的原型链继承的例子我们不难方面以下几点:
1、和C#,Java等面向对象语言不同,它并不是继承类的机构的方式,而是将一个父对象实例赋值给子对象的原型指针而实现继承的
2、使用该方式的继承不仅会继承来自实例中的方法,而且会继承其属性,并且该属性的可以说是静态的,即在一个子对象的实例中改变,则其它实例中也会改变。
3、我们知道默认的原型对象里会有一个constructor属性,该属性指向的是函数的本身,而现在整个原型对象被重写了,子对象的构造函数属性指向了父对象的函数,即现在sub的constructor指向的是SuperType函数。
2、借用构造函数
使用原型链可以实现继承,但是这样的继承并不是完美的,大部分时候我们并不希望继承的属性是存在原型对象中的,因为这样带来的负面效果是牵一发而动全身,改变一处而处处改变,而这样的实现是无法满足一些特定的需求的。所以如果我们要子对象继承父对象中的属性,而又希望每个子对象中的属性是独立的话,则必须要考虑其他的实现方式。在此引申出另一种实现方法:借用构造函数。这种方式可以解决属性继承的问题。
function SuperType(){
this.name = "Li Lei";
this.sayHi = function() {
alert("Hi");
};
}
SuperType.prototype.show = function() {
alert("Hello world");
};
function SubType(){
SuperType.call(this);
}
var sub = new SubType();
sub.sayHi();//Hi
alert(sub.name);//Li Lei
alert(sub.constructor == SubType);//true
alert(sub.show);//undefined
我们可以看到实现这种继承的方式只是在子类型的构造函数的内部调用父类型的构造函数而已。能实现这种方式的主要原因是函数是在特定环境中执行代码的对象,因此通过使用apply()和call()方法也可以在新创建的对象上执行构造函数。浅显点来说,现在做的事不过是将子对象当前作用域赋值给函数SuperType,然后执行SuperType函数,因为此时SuperType函数里的this为sub,所以才会使得sub有了name和sayHi属性。
这种实现方式有以下几点也需要注意:
1、该实现没有改变原型对象
2、原型对象中的constructor指向的仍是子对象函数类型本身
3、父类型中的原型对象对子类型是不可见的
所以当我们想子类型能够继承父类型的原型时,借用构造函数方式是不能满足需求的。
3、组合继承
顾名思义组合继承就是原型链+借用构造函数继承了,它有时候也叫经典伪继承。其思路是使用原型链继承父类型的原型属性和方法,使用借用构造函数来实现对实例属性和方法的继承。话不多说,看一段代码。
function SuperType(){
this.name = "Li Lei";
}
SuperType.prototype.show = function() {
alert("Hello world");
};
function SubType(){
SuperType.call(this);
}
SubType.prototype = new SuperType();
var sub = new SubType();
alert(sub.name);//Li Lei
alert(sub.constructor == SuperType);//false
sub.show();//Hello world
在这里虽然SubType的原型中虽然也存在属性name,但是由于利用借用构造函数方式,在构造函数内部又定义了name,此时这个那么是覆盖了原型中的name的。由此可见,组合继承融合了原型链和借用构造函数的优点,避免了各自产生的缺陷。但是组合继承也并非是完美的,它调用了两次父类型的构造函数,可能会产生冗余的方法和属性,例如上面的例子中,原型对象和实例对象都有name属性。
4、原型式继承
如果你不想创建一个构造函数,而只是想让一个对象和另一个对象相似的话,就可以使用原型式继承了。
function create(obj){
function F(){};
F.prototype = obj;
return new F();
}
var people = {
name: "Li Lei",
friends: ["Han Meimei", "Jim Green"]
}
var tom = create(people);
tom.name = "Tom";
tom.friends.push("Li Lei");
var lucy = create(people);
lucy.name = "Lucy";
lucy.friends.push("Tom");
alert(people.friends);//Han Meimei,Jim Green,Li Lei,Tom
在ECMAScript5中通过新增了Object.create()的方法规范了原型式继承,该方法接受两个参数,一个是用作新对象原型的对象,一个是为新对象定义额外属性的对象,在只传入一个对象的情况下,该方法和上面示例中的方法create中的表现是一致的。
var people = {
name: "Li Lei",
friends: ["Han Meimei", "Jim Green"]
}
var tom = Object.create(people);
tom.name = "Tom";
tom.friends.push("Li Lei");
var lucy = Object.create(people);
lucy.name = "Lucy";
lucy.friends.push("Tom");
alert(people.friends);//Han Meimei,Jim Green,Li Lei,Tom
5、寄生式继承
这是一种与原型式继承紧密相关的继承,与寄生构造模式和工厂模式类似。相当于是加强版的原型式继承。
function create(obj){
function F(){};
F.prototype = obj;
return new F();
}
function createParasitic(obj){
var para = create(obj);
para.sayHi = function(){
alert("Hi");
}
return para;
}
var people = {
name: "Li Lei",
friends: ["Han Meimei", "Jim Green"]
}
var lily = createParasitic(people);
lily.sayHi();// Hi
这种做法的结果是不仅得到了一个与另外一个对象相似的对象,而且在这个对象上添加了自己想要的方法。
6、寄生组合式继承
前面我们说了组合继承,它是JavaScript中最常用的模式,而组合继承的不足就是调用了两次构造函数,结果是产生了冗余的属性或者方法。
function SuperType(){
this.name = "Li Lei";
}
SuperType.prototype.show = function() {
alert("Hello world");
};
function SubType(){
SuperType.call(this); //第二次调用构造函数
}
SubType.prototype = new SuperType(); //第一次调用构造函数
var sub = new SubType();
alert(sub.name);//"Li Lei"
alert(sub.constructor == SuperType);//true
sub.show();//Hello world
所谓的寄生组合式,指的是使用借用构造函数的方式来继承属性,使用原型链混合的方式来继承方法。它的主题思想就是不必为了子类型的原型而调用父类型的构造函数,我们所需要的只是父类型的原型对象的一个副本而已。其实现方式如下:
function create(obj){
function F(){};
F.prototype = obj;
return new F();
}
function inheritPrototype(subType, superType){
var proto = create(superType.prototype);
proto.constructor = subType;
subType.prototype = proto;
}
下面我们再来看一个使用寄生组合继承的例子。
function create(obj){
function F(){};
F.prototype = obj;
return new F();
}
function inheritPrototype(subType, superType){
var proto = create(superType.prototype);
proto.constructor = subType;
subType.prototype = proto;
}
function SuperType(){
this.name = "Li Lei";
}
SuperType.prototype.show = function() {
alert("Hello world");
};
function SubType(){
SuperType.call(this); //第二次调用构造函数
}
inheritPrototype(SubType, SuperType);
var sub = new SubType();
alert(sub.name);//"Li Lei"
alert(sub.constructor == SubType);//true
sub.show();//Hello world
至此,基本的继承方式已经说完,每一种方式都有利有弊,关键在于用在合适的地方,才能发挥无限的威力。