借鉴于《JavaScript高级程序设计(第4版)》
-
原型链继承
-
盗用构造函数
-
组合继承
-
原型式继承
-
寄生式继承
-
寄生组合式继承
原型链继承
代码如下:
function SuperType(){
this.colors = ['red','orange']
}
function SubType(){}
SubType.prototype = new SuperType();
此种继承方式会出现以下两个问题:
-
子类会一并继承父类的属性,并且所有的子类实例对象想要访问父类的属性xxx的时候,该属性都指向相同的引用(SubType.prototype.xxx)
举例:见下面代码
let instance1 = new SubType(); let instance2 = new SubType(); // 创建两个子类实例对象instance1和instance2 instance1.colors.push('black'); console.log(instance1.colors); // 'red,orange,black' console.log(instance2.colors); // 'red,orange,black'
这不是我们想要的效果,此处我们希望的效果应该是console.log(instance2.colors); // 'red,orange',即每个子类实例对象的colors可以继承自SuperType,同时这些colors指向不同的引用,改变instance1的colors的时候,instance2的colors不受干扰。
但是由于实际上所有的子类SubType的实例对象指向colors的时候,指向的都是SubType.prototype.colors,所以相当于所有的子类实例对象共享该属性
console.log(instance1.colors === SubType.prototype.colors); //true console.log(instance2.colors === SubType.prototype.colors); //true
原型链继承存在着这样的问题与弊端。
-
子类在实例化的时候,无法按照子类的需求向父类的构造函数进行动态传参
因为父类已经在 SubType.prototype = new SuperType(); 这里进行了实例化了,父类只会实例化一次,不会多次构造。同时由于所有的子类实例化对象都通过SubType.prototype来访问父类属性与方法,传参的时候同样会出现上述1中的问题,其他的子类对象在访问父类属性时会受到影响。
-
因此该方式可能只适合继承父类上定义的方法,没有定义可变属性的情况。
盗用构造函数
基本思路:在子类构造函数中调用父类构造函数。代码如下:
function SuperType(){
this.colors = ['red','blue','green'];
}
function SubType(){
SuperType.call(this);
// 执行效果相当于
// this.colors = ['red','blue','green'];
}
let instance1 = new SubType();
let instance2 = new SubType();
instance1.colors.push('black');
console.log(instance1.colors); // ['red','blue','green','black']
console.log(instance2.colors); // ['red','blue','green']
这里的instance1.colors和instance2.colors指向不同的引用,所以不会出现上述原型链继承所出现的问题
对比于原型链继承,盗用构造函数实现继承的好处是:
-
可以实现子类向父类的构造函数传递参数
-
见下面代码
function SuperType(name){ this.name = name; } function SubType(name){ SuperType.call(this,name); } let instance = new SubType('Alice'); console.log(instance.name); //'Alice'
同时该方法存在着问题:
-
由于子类型是调用父类型的构造函数来实现继承,所以父类中所有的属性和方法都必须在自己的构造函数中实现,所以这些定义在构造函数里面的函数没有办法做到重用。
-
由于子类调用父类构造函数,实际上只是执行了父类的构造函数里面的代码,将父类的this变成了子类的this来实现继承,只是继承了父类中定义的属性和方法。因此并没有继承父类原型上的方法。
组合继承
结合了原型链和盗用构造函数,基本思路是使用原型链继承原型上的属性和方法,通过盗用构造函数继承实例属性,结合了上述两种方法的优点同时避免了上述方法的缺点。
代码如下:
function SuperType(name){
this.name = name;
this.colors = ['red','blue'];
}
// 定义父类构造函数
SuperType.prototype.sayName = function(){
console.log(this.name);
}
// 父类显式原型对象上的sayName函数
function SubType(name,age){
SuperType.call(this,name); // 调用父类的构造函数
// 获得 this.name = name; this.colors = ['red','blue']
this.age = age;
}
// 定义子类构造函数
SubType.prototype = new SuperType(); // 实际效果是:
// SubType.prototype.name = undefined;
// SubType.prototype.colors = ['red','blue'];
SubType.prototype.sayAge = function(){
console.log(this.age);
}
let instance1 = new SubType("Alice",19);
let instance2 = new SubType("Greg",20);
instance1.colors.push('yellow');
console.log(instance1.colors); //'red,blue,yellow'
console.log(instance1.colors); // 'red,blue'
// 因为SubType在new的时候调用了SuperType的构造函数,创建了不同的colors
instance1.sayName(); //'Alice'
// 实际上是instance1.__proto__.__proto__.sayName()
instance1.sayAge(); //'19'
// 实际上是instance1.__proto__.sayAge();
对于instance1,当访问name的时候,子类实例对象内部具有内部属性name='Alice',子类实例的隐式原型对象上同样具有name=undefined。
在访问类属性的时候,首先查找类内部是否有该属性,如果有的话直接使用该类内部的属性,找不到的情况下才会沿着原型链去寻找该属性。
因此在访问的时候,子类实例对象内部的name属性会覆盖掉原型上的属性,实现各个子类的实例属性在读or写的时候互不干扰的效果,因为他们访问的是自己类内部的name属性,不会读取到原型上的name属性。
原型式继承
Object.create()函数将原型式继承的概念规范化
接收两个参数,1.该对象指向的原型p 2.给新对象定义额外属性的对象(可选)
意思是创建一个对象,将该对象的原型对象指向参数1。
let person = {
name:'Alice',
friends:['A','B','C']
}
let anotherPerson1 = Object.create(person);
anotherPerson1.name = 'Greg';
anotherPerson1.friends.push('D');
// anotherPerson1.__proto__name === person.name 'true'
let anotherPerson2 = Object.create(person);
// anotherPerson2.__proto__.name === person.name 'true'
anotherPerson2.friends.push('E')
console.log(person.name); // Alice
console.log(person.friends); // ['A','B','C','D','E']
该方法本质上与原型链继承是一样的,但是表达更为简便:当具有某个对象A的时候,不需要单独创建产生该对象的构造函数来实现继承,可以直接将这个对象A传进Object.create()函数里面,然后返回一个新的对象B,该对象B的隐式原型对象是A,实现继承。
寄生式继承
与原型式继承类似,寄生式继承的思路类似于寄生构造函数和工厂模式,基本的寄生继承模式如下:
function createAnother(original){
let clone = object(original);
// 相当于 function F(){}; F.prototype = original; return new F();
// clone 继承了original的方法,同时扩充了自身的方法
clone.sayHi = function(){
console.log('hi');
}
// 缺陷:在这里添加函数会导致函数难以重用,与盗用构造函数类似
return clone;
}
寄生式组合继承
组合继承的效率问题:父类构造函数会被调用两次(一次是创建子类原型,一次是在子类构造函数里面),这就导致:
通过上面组合继承,可以得知一个SubType的实例对象上有name属性,同时其原型对象上也有name属性,在具体访问属性的时候,实例对象中的属性将原型对象上的掩盖掉了。
寄生式组合继承解决这个问题。它通过盗用构造函数来继承属性,通过混合式原型链继承方法(就是构造函数继承属性,原型链继承方法)(通过盗用构造函数来继承父类内部的内容,通过原型链继承来继承父类原型上的内容)。
不通过调用父类的构造函数来给子类原型赋值,而是取得父类原型的一个副本。
寄生式组合继承的基本模式如下
function inheritPrototype(subType,superType){
let prototype = object(superType.prototype); // 创建父类原型的一个副本
prototype.constructor = subType; // 设置constructor属性
subType.prototype = prototype; // 设置子类原型:父类对象实例
// 此处继承到的是父类原型里面的方法,父类中的属性没有继承
// 子类如何继承父类中的属性:在子类的构造函数中调用父类的构造函数来获取
}
使用:
function SuperType(name){
this.name = name;
this.colors = ['red','blue','green'];
}
SuperType.prototype.sayName = function(){
console.log(this.name);
}
function SubType(name,age){
SuperType.call(this,name); // 在这里调用了一次构造函数,继承父类的属性
this.age = age;
}
inheritPrototype(subType,superType); //实现子类原型的设置,继承父类的方法
该方案在调用子类SubType的构造函数的时候,调用了一次父类SuperType构造函数,避免了在子类原型上会出现name 和 colors属性的情况(即便他们出现,也不会被访问到,因此在原型上这两个属性没有用处,不需要继承下来),效率相比于组合继承更高。