注:本文中,父类型即是超类型。
一、原型链继承
1、基本思想:利用原型让一个引用类型继承另一个引用类型的属性和方法,让新实例的原型等于父类的实例,重写原型对象
2、特点:
- 实例可以继承的属性有:实例的构造函数的属性、父类构造函数的属性、父类原型的属性。但是新实例不会继承父类实例的属性和方法
- 子类的实例也是父类的实例
3、缺点:
- 无法实现多继承
- 创建子类型的实例时,不能向父类型的构造函数中传递参数
- 所有新实例都会共享父类实例的属性(因此要在构造函数中而不是原型对象中定义属性)
4、实现模式:
function Father(){
this.name = "CD";
this.sex = "m";
}
Father.prototype.getFather = function(){
console.log(this.name + "," + this.sex);
}
function Son(){
this.name = "AB";
this.age = 11;
}
Son.prototype = new Father();
Son.prototype.getSon = function(){
console.log(this.name);
}
var instance = new Son();
instance.getFather();//AB,m
解析:
- 定义了一个父类Father和一个子类Son,分别给Father和Son的原型写一个方法getFather()和getSon()。并且手动设置子类的原型是父类的实例,这个过程中重写了子类原型。重写后子类原型指向父类原型,父类原型的constructor属性指向Father,因此子类原型的constructor属性指向Father。
- 再使用子类构造函数创建一个实例instance,这也即是继承的实现过程。现在,存在于父类的实例中的所有属性和方法也存在于子类的原型中了。
- 在实例上调用getFather()。引擎会在instance的原型链上查找这个方法,查找过程为:实例instance——>实例instance的原型Son.prototype——>子类Son的原型Father.prototype,查找到此处停止查找,因为在子类的原型:父类上找到了该方法。
- 然后执行该方法,该方法中要获取两个值:name与sex。其中name将从instance中直接找到,因为这是实例从构造函数中继承而来的;同样,sex存在于Son.prototype中,这是因为子类原型是父类的实例,该属性理所应当在该实例中。
- 所以输出:AB,m
5、确定原型与实例的关系:instance是Object、父类、子类中任何一个类型的实例。
alert(instance instanceof Object);//true
alert(instance instanceof Person);//true
alert(instance.constructor == Object);//true
alert(instance.constructor == Person);//false
alert(Object.prototype.isPrototypeOf(instance));//true
alert(f.prototype.isPrototypeOf(instance));//true
alert(s.prototype.isPrototypeOf(instance));//true
6、使用时要注意:
- 子类型有时需要覆盖父类型中的某个方法,或者需要添加父类型中不存在的某个方法。但不管怎样,给原型添加方法的代码一定要放在替换原型的语句之后。
- 通过原型链实现继承时,不能使用对象字面量创建原型方法,这样做会重写原型链。
二、构造函数继承
1、基本思想:在子类构造函数的内部调用父类构造函数,通过call改变父类中this的指向,相当于赋值父类的实例属性给子类
2、特点:
- 可以实现多继承
- 在子实例中可以向父实例传参
- 解决了原型链继承中子类实例共享父类引用类型值的问题
3、缺点:
- 只继承了父类构造函数的属性和方法,无法继承父类原型的属性和方法
- 无法实现构造函数的复用,即每次都要重新调用
- 每个新实例都要父类构造函数的副本,占内存
- 实例并不是父类的实例,只是子类的实例。
4、实现模式:
function Father(){
this.names = ["AB","CD"];
}
function Son(){
//继承Father,借调了超类型的构造函数
Father.call(this);
}
var instance1 = new Son();
instance1.names.push("EF");
console.log(instance1.names);//AB,CD,EF
var instance2 = new Son();
console.log(instance2.names);//AB,CD
解析:定义了一个父类Father,再定义一个子类Son,在子类内部通过call()调用父类构造函数来继承父类。这样就可以在新子类型对象上执行父类型中定义的所有对象初始化代码,结果子类型的每个实例都会具有自己属性的副本。
5、传递参数:在子类型构造函数中向超类型构造函数传递参数
function Father(){
this.name=name;
}
function Son(){
Father.call(this,"CD");
this.age = 21;
}
var instance = new Son();
console.log(instance.name);//CD
console.log(instance.age);//21
三、组合继承
1、基本思想:使用原型链实现对原型属性和方法的继承,通过构造函数实现对实例属性的继承。这样通过在原型上定义方法实现了函数复用,又能够保证每个实例都有自己的属性。
2、特点:
- 既是子类的实例,也是父类的实例
- 可以继承父类原型上的属性
- 创建子类型的实例时,可以向父类型的构造函数中传递参数
- 可以实现复用
- 属性私有,即解决了原型链继承中子类实例共享父类引用类型值的问题
3、缺点:
- 调用了两次父类构造函数,耗内存(一次在创建子类型原型时,另一次在子类型构造函数内部)
- 子类的构造函数会代替原型上的父类构造函数,调用子类型构造函数时,会重写父类型的属性
- 子类同时拥有父类的属性,但是子类属性会覆盖父类属性
4、实现模式:
function Father(sex){
this.sex = sex;
this.names = ["AB,"CD"];
}
Father.prototype.getFather = function(){
console.log(this.sex);
}
function Son(sex,age){
Father.call(this,sex);
this.age = age;
}
Son.prototype = new Father();
Son.prototype.constructor = Son;
Son.prototype.getSon = function(){
console.log(this.age);
}
var instance1 = new Son("m",11);
instance1.names.push("EF");
console.log(instance1.names);//AB,CD,EF
instance1.getFather();//m
instance1.getSon();//11
var instance2 = new Son("w",8);
console.log(instance2.names);//AB,CD
instance2.getFather();//w
instance2.getSon();//8
解析:
- 超类型构造函数定义了两个属性,超类型原型定义了一个方法。子类型构造函数在调用超类型构造函数时传入了sex参数,紧接着又定义了自己的属性age。
- 将超类型的实例赋值给子类型原型,然后又在新原型上定义了方法。这样使两个不同的实例分别拥有自己的属性,有可以使用相同的方法。
四、原型式继承
1、基本思想:借助原型可以基于已有的对象创建新对象,同时还不必创建自定义类型。也就是,用一个函数包装一个对象,然后返回这个函数的调用,这个函数就变成一个可以随意添加属性的实例或对象。
2、特点:
- 类似于复制一个对象,用函数来包装
- 直接通过对象生成一个集成该对象的对象
3、缺点(不是类式继承,缺少了类的概念):
- 所有实例都会继承原型上的属性
- 无法实现复用(新实例属性都是后面添加的)
4、实现模式:
function object(o){
function F(){}
F.prototype = o;
return new F();
}
var person = {
name:"one",
friends:["AB","CD","EF"]
};
var otherPerson1 = object(person);
otherPerson1.name = "two";
otherPerson1.friends.pop("EF");
var otherPerson2 = object(person);
otherPerson2.name = "three";
otherPerson2.friends.pop("CD");
//均输出:["AB"]
console.log(person.friends);
console.log(otherPerson1.friends);
console.log(otherPerson2.friends);
解析:
- 在object()内部,创建一个临时构造函数,然后将传入的对象作为这个构造函数的原型,最后返回这个临时类型的新实例。本质是object()对传入其中的对象进行了一次浅复制。这种继承必须有一个对象可以作为另一个对象的基础,将这个对象传给object(),然后再根据需求修改得到的对象。包含引用类型值的属性始终都会共享相应的值。
- person作为其他对象的基础传入object()中,该函数返回一个新对象。即person是新对象的原型。person中的属性与新对象共享,即相当于创建了person对象的两个副本。
5、ES5通过新增Object.create()规范化了原型式继承。该方法接收两个参数,一个用作新对象原型的对象和一个为新对象定义额外属性的对象。在传入一个参数的情况下,Object.create()与object()方法的行为相同。第二个参数与Object.defineProperties()的第二个参数格式相同:每个属性都是通过自己的描述符定义的。以这种方式指定的任何属性都会覆盖原型对象上的同名属性
五、寄生式继承
1、基本思想:通俗来说就是给原型式继承外面套了一个壳。创建一个仅用于封装继承过程的函数,该函数在内部以某种方式来增强对象,最后再像真的是它做了所有工作一样返回对象。任何能够返回新对象的函数都适用于此模式。
2、特点:
- 没有创建自定义类型,是原型式继承的一种拓展
3、缺点:
- 无法复用
- 没有用到原型,没有类的概念
4、实现模式:
function object(o){
function F(){}
F.prototype = o;
return new F();
}
function createAnother(o){
var clone = object(o); //通过调用函数创建一个新对象
clone.getH = function(){ //以某种方式来增强这个对象
alert("hhh");
}
return clone; //返回这个对象
}
var person = {
name:"AB",
friends:["CD","EF"]
};
var anotherPerson = createAnother(person);
anotherPerson.getH();//hhh
解析:createAnother()是用于封装继承过程的函数,接收要作为新对象基础的对象为参数。基于person返回一个新对象,不仅具有person的属性和方法,还有自己的方法。
六、寄生组合式继承
1、基本思想:通过借用构造函数来继承属性,通过原型链的混成形式来继承方法。不必为了指定子类型的原型而调用超类型的构造函数,我们所需的无非就是超类型原型的一个副本。本质就是使用寄生式继承来继承超类型的原型,然后再将结果指定给子类型的原型。
2、特点:
- 解决了组合继承带两份属性的问题子类同时拥有超类的属性,但是子类属性会覆盖超类属性
3、缺点:
- 繁琐
4、实现模式:
function object(o){
function F(){}
F.prototype = o;
return new F();
}
function inheritPrototype(son,father){
var prototype = object(father.prototype);
prototype.constructor = son;
son.prototype = prototype;
}
function Father(name){
this.name = name;
this.colors = ["red","green","blue"];
}
Father.prototype.getFather = function(){
console.log(this.name);
}
function Son(name,age){
Father.call(this,name);
this.age=age;
}
inheritPrototype(Son,Father);
Son.prototype.getSon(){
console.log(this.age);
}
解析:inheritPrototype()内部,创建了超类型原型的副本并为其添加constructor属性,从而弥补因重写原型而失去默认的constructor属性;最后再将这个新创建的对象赋值给子类型的原型。在这种模式下,只需要调用一次超类型构造函数,并且避免了在子类型原型上创建不必要的多余的属性。与此同时,原型链保持不变,还可以正常使用instanceof和isPrototypeOf()。普遍认为这种方式是引用类型最理想的继承方式。