JS继承方式总结

根据《JS高级程序设计·第三版》第六章内容,总结了下JS的继承方式

Part1. 理解Prototype原型对象

无论什么时候只要创建了一个新函数,JS就会为该函数创建一个prototype属性指向此函数原型对象。
同时,原型对象会自动获得一个constructor属性,指向prototype属性所在函数指针。
当创建一个构造函数,并调用该构造函数创建一个新实例后,实例内部也将有一个指针(ES5中叫做[[Prototype]]),
Firefox,Safari,Chrome上为__proto__属性,其指向构造函数的原型对象。
其在原型对象上定义的属性为所有对象实例共享,称为原型属性。

1
2
3
4
5
6
7
8
9
10
function Person(){
}
Person.prototype.name = "Nicholas";
Person.prototype.sayName = function(){
alert(this.name);
};
var person1 = new Person();
var person2 = new Person();

上述代码的实例、构造函数、和原型对象之间关系如下图所示:

js_understand_prototype.png

Part2. JS继承方式总结

原型链继承

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function SuperType(){
this.property = true;
}
SuperType.prototype.getSuperValue = function(){
return this.property;
};
function SubType(){
this.subproperty = false;
}
//直接重写prototype对象实现继承SuperType
SubType.prototype = new SuperType();
Sub.prototype.getSUbValue = function(){
return this.subproperty;
};
var instance = new SubType();
alert(instance.getSuperValue()); //true

上述代码的继承是通过以一个SuperType实例替换SubType默认原型实现。
要注意此时:instance.constructor指向SuperType.

存在的问题

  1. 父类中实例属性成为子类原型属性,被子类所有实例共享。

  2. 创建子类型实例时,不能向父类型构造函数传递参数。

借用构造函数继承(经典/伪造对象继承)

1
2
3
4
5
6
7
8
9
10
11
12
13
function SuperType(name){
this.name = name;
}
function SubType(){
//调用父类构造函数实现继承,并可以传递参数
SuperType.call(this, "Nicholas");
//再添加子类实例属性、方法
}
var instance = new SubType();
alert(instance.name); //"Nicholas"

借用构造函数继承是通过在子类型构造函数内部使用apply或call调用父类型构造函数,
从而在新创建对象上执行父类构造函数来实现。其可以向父类型构造函数传递参数。

存在的问题

父类型原型中定义的方法对子类型不可见。

组合继承

也称为伪经典继承,将原型链和借用构造函数继承组合起来使用,是最常用的继承方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function SuperType(name){
this.name = name;
}
SuperType.prototype.sayName = function(){
alert(this.name);
};
function SubType(name, age){
//借用构造函数继承实例属性
SuperType.call(this, name);
this.age = age;
}
//原型链继承原型方法
SubType.prototype = new SuperType();

原型式继承

不需要额外创建新的构造函数,让新对象与原对象保持类似,但新对象会共享原对象的所有属性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function object(o){
function F(){} //创建一个临时性构造函数
F.prototype = o; //设置原型
return new F(); //返回临时类型实例
}
var person = {
name: "Nicholas",
friends: ["Amy", "Bruno", "Candy"]
};
var subPerson1 = object(person);
subPerson1.friends.push("Danel");
var subPerson2 = object(person);
subPerson2.friends.push("Ely");
//因为subPerson1, subPerson2共享同一个原型对象,所以:
alert(subPerson2.friends); //Amy, Bruno, Candy, Danel, Ely
alert(Person.friends); //Amy, Bruno, Candy, Danel, Ely

ES5中Object.create()方法规范化了原型式继承。
其接收两个参数:

  1. 用做新对象原型的对象

  2. [可选]为新对象定义额外属性的对象

寄生式继承

即创建一个仅用于封装继承过程的函数,该函数在函数内部以某种方式增强对象,最后返回此对象。
因为其仅在原对象基础上增强而不是自定义类型和构造函数,故称为寄生式继承。

但其不能做到函数复用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function object(o){
function F(){}
F.prototype = o;
return new F();
}
function createAnother(original){
var clone = object(original); //可以使用任何返回新对象的函数
clone.sayHi = function(){ //增强返回对象
alert("Hi");
};
return clone; //返回增强对象
}
var person = {
name: "Nicholas"
};
var anotherPerson = createAnother(person);

寄生组合式继承

前面说过,组合继承是JS最常用的继承模式,但其也存在一个问题:

无论什么情况下都会调用两次父类构造函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function SuperType(name){
this.name = name;
}
SuperType.prototype.sayName = function(){
alert(this.name);
};
function SubType(name, age){
SuperType.call(this, name);//第二次调用
this.age = age;
}
SubType.prototype = new SuperType();//第一次调用

这样导致我们在子类的prototype上创建了无用的SuperType的实例属性,
因为所有的父类实例属性都会在第二次使用call调用时被覆盖

对于这个问题的解决办法就是:寄生组合式继承

其和组合式继承区别在于,就是采用寄生方式去用父类型的prototype去覆盖子类型的prototype。
而不是使用父类型的实例去重设子类型的prototype。

因为我们仅仅需要在父类型prototype上的原型属性,而父类型的实例属性则之后通过借用构造函数方式继承。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function inheritPrototype(subType, superType){
var prototype = object(superType.prototype); //创建父类型"副本"
prototype.constructor = subType; //重设constructor属性
subType.prototype = prototype; //设置子类型prototype
}
function SuperType(name){
this.name = name;
}
SuperType.prototype.sayName = function(){
alert(this.name);
};
function SubType(name, age){
SuperType.call(this, name); //继承父类实例属性
this.age = age;
}
inheritPrototype(SubType, SuperType); //继承父类原型属性