js继承的6种模式

1,170 阅读6分钟

js继承

1. 原型链继承

其基本思想是利用原型让一个引用类型继承另一个引用类型的属性和方法
构造函数、原型和实例之间的关系:每个构造函数都有一个原型对象,原型对象都包含一个指向构造函数的指针,而实例都包含一个原型对象的指针。
大家可以根据下面这张图片,并结合上面这句话一起理解原型链,这里就不详细描述了。

来看看原型链继承的代码

function SuperType(){
    this.property = true; 
} 
SuperType.prototype.getSuperValue = function(){ 
     return this.property; 
}; 
function SubType(){ 
     this.subproperty = false; 
} 
// 继承了 SuperType 
SubType.prototype = new SuperType(); 
SubType.prototype.getSubValue = function (){ 
     return this.subproperty; 
}; 
var instance = new SubType(); 
console.log(instance.getSuperValue()); //true

让我们看最关键的代码:SubType.prototype = new SuperType()
为什么这一段代码就实现了继承?
我们先来看看new操作符的原理

function create(Con, ...args) {
      let obj = {}
      obj.__proto__ = Con.prototype
      let result = Con.apply(obj, args)
      return result instanceof Object ? result : obj
}

提取出new的关键代码:obj.proto = Con.prototype,并将其嵌套在原型链继承的关键代码中,其实就是SubType.prototype.proto = SuperType.prototype

根据上述的推理我们可以画出如下关系图

proto.PNG 我们来验证一下

console.log(instance.__proto__ === SubType.prototype) // true
console.log(SubType.prototype.__proto__ === SuperType.prototype) // true

可以发现与我们的猜想成立。

缺点

原型链虽然很强大,可以用它来实现继承,但它也存在一些问题。其中,最主要的问题来自包含引用类型值的原型。引用类型值的原型属性会被所有实例共享。

function SuperType(){
    this.colors = ["red", "blue", "green"];
}
function SubType(){
}
// 继承了 SuperType
SubType.prototype = new SuperType();
var instance1 = new SubType();
instance1.colors.push("black");
console.log(instance1.colors); //"red,blue,green,black"
var instance2 = new SubType();
console.log(instance2.colors); //"red,blue,green,black"

为什么会这样?
因为通过原型链继承之后,SubType.prototype相当于Super的一个实例。当我们创建一个SubType的实例的时候,相当于创建了一个SubType.prototype.colors,当你更改color的时候其实就是更改SubType原型上的colors,所以导致SubType的原型属性会被所有实例共享。

2. 借用构造函数继承

基本思想:在子类型构造函数的内部调用超类型构造函数。

function SuperType(){
    this.colors = ["red", "blue", "green"];
}
function SubType(){
    // 继承了 SuperType
    SuperType.call(this);
}

var instance1 = new SubType();
instance1.colors.push("black");
console.log(instance1.colors); //"red,blue,green,black"
var instance2 = new SubType();
console.log(instance2.colors); //"red,blue,green"

实际上是在新创建的 SubType 实例的环境下调用了 SuperType 构造函数

优点

  1. 父类的引用属性不会被共享
  2. 可以在子类构造函数中向父类传参数

缺点

方法都在构造函数中定义,每次创建实例都会创建一遍方法。达不到复用

3.组合继承

定义:将原型链和借用构造函数的技术组合到一块,从而发挥二者之长的一种继承模式。
基本思想:是使用原型链实现对原型属性和方法的继承,而通过借用构造函数来实现对实例属性的继承。

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;
}
// 继承方法,第一次继承
SubType.prototype = new SuperType();
// 重写SubType.prototype的constructor属性,指向自己的构造函数SubType
SubType.prototype.constructor = SubType;
SubType.prototype.sayAge = function(){
    console.log(this.age);
};
var instance1 = new SubType("Nicholas", 29);
instance1.colors.push("black");
console.log(instance1.colors); //"red,blue,green,black"
instance1.sayName(); //"Nicholas";
instance1.sayAge(); //29
var instance2 = new SubType("Greg", 27);
console.log(instance2.colors); //"red,blue,green"
instance2.sayName(); //"Greg";
instance2.sayAge(); //27

将公共方法sayName挂载在父类(SuperType)的原型上,通过原型链继承就可以调用该方法,并且得到了复用。
为防止实例属性被共用,通过借用构造函数来继承

优点

组合继承避免了原型链和借用构造函数的缺陷,融合了它们的优点,成为JavaScript 中最常用的继承模式。

缺点

zuhe.png

结合上图,发现创建的实例和原型上存在两份相同的属性
这样的话会有什么问题吗?我举个例子

delete instance1.colors
console.log(instance1.colors) // "red,blue,green"

看上图,可以发现当我们删除实例的colors属性的时候,再去查询的时候,实际上还能访问到,但我们的意愿是访问把到。有值是因为原型上还有colors。

缺点:都会调用两次超类型构造函数:一次是在创建子类型原型的时候,另一次是在子类型构造函数内部。导致其原型中会存在两份相同的属性/方法。

4. 原型式继承

利用一个空对象作为中介,将某个对象直接赋值给空对象构造函数的原型。

function object(o){ 
    function F(){} 
    F.prototype = o; 
    return new F(); 
}

本质上讲object()对传入其中的对象执行了一次潜复制

var person = { 
    name: "Nicholas", 
    friends: ["Shelby", "Court", "Van"] 
}; 
var anotherPerson = object(person); 
anotherPerson.name = "Greg"; 
anotherPerson.friends.push("Rob"); 
var yetAnotherPerson = object(person); 
yetAnotherPerson.name = "Linda"; 
yetAnotherPerson.friends.push("Barbie"); 
console.log(person.friends); //"Shelby,Court,Van,Rob,Barbie"

ES5中存在Object.create()的方法,能够代替上面的object方法。
缺点不多赘述跟原型链的缺点一样。

5. 寄生式继承

创建一个仅用于封装继承过程的函数,该函数在内部以某种方式来增强对象,最后再像真地是它做了所有工作一样返回对象。

function createAnother(original){
    var clone = object(original); // 通过调用函数创建一个新对象 
    clone.sayHi = function(){ // 以某种方式来增强这个对象 
        console.log("hi"); 
    };
    return clone; //返回这个对象 
}

var person = { 
    name: "Nicholas", 
    friends: ["Shelby", "Court", "Van"] 
}; 
var anotherPerson = createAnother(person);
anotherPerson.sayHi(); //"hi"

该继承与原型式继承紧密相关,在其基础上添加自己的属性/方法

缺点(同原型式继承)

与构造函数模式类似,添加的方法得不到复用,降低效率

5. 寄生组合式继承

组合继承会调用两次超类型构造函数,而寄生组合式继承可以完美的解决该问题。
基本思想:使用寄生式继承来继承超类型的原型,然后再将结果指定给子类型的原型。

function object(o){ 
    function F(){} 
    F.prototype = o; 
    return new F(); 
}
function inheritPrototype(subType, superType){ 
    var prototype = object(superType.prototype); // 创建对象
    prototype.constructor = subType; // 增强对象
    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.prototype.sayAge = function(){ 
    console.log(this.age); 
};
var instance1 = new SubType("xyc", 23);
var instance2 = new SubType("lxy", 23);
instance1.sayName()
instance1.colors.push("2"); // ["red", "blue", "green", "2"]
instance1.colors.push("3"); // ["red", "blue", "green", "3"]

inheritPrototype函数内部,第一步是创建超类型原型的一个副本。第二步是为创建的副本添加 constructor 属性,从而弥补因重写原型而失去的默认的 constructor 属性。最后一步,将新创建的对象(即副本)赋值给子类型的原型。
从创建对象那一步,一开始感觉F函数和Super函数差不多,具体看看object(SuperType.prototype)new SuperType()的区别

jszh.png

可以发现两者是有区别的,使用一个空对象(F)作为中介的好处就是不会复制Super的属性,这样就避免了存在两份一样的属性

我的大致理解

    1. 创建一个空对象(F)作为中介,让其原型指向与SuperType指向相同,避免了存在两份一样的属性
    1. 再通过原型链继承让subType的原型等于F的实例,继承SuperType原型上的属性/方法,避免创建实例的时候重复创建。
    1. 最后在创建实例的时候使用构造函数模式继承父类的属性

开发人员普遍认为寄生组合式继承是引用类型最理想的继承范式。