【面试】-- 讲一讲ES5的几种继承

134 阅读8分钟

在ES6之前,不存在类的语法糖,那在ES5之前是如何实现继承的呢?

(内容来自红宝书,红宝书中讲的已经十分详细了。以下是自己的学习思考和总结)

原型链继承

我们知道,当我们访问一个对象的属性时,如果在自身属性找不到的话,就会继续沿着原型链查找,直到找到为止。

那如果我修改原型链,让子类的实例的原型指向父类的实例,这样组成了一条原型链, 是不是子类就可以访问父类的属性和方法,那是不是就是说明实现了继承?

所以原型链的核心就在于:修改自身的prototype属性指向要继承的对象的实例。

//父类
function SuperType(){
    
};
 
//子类
function SubType(){
    
};
 
// 精髓:将SuperType的实例 赋值给 Subtype的protype属性。 重写了SubType的protype。实现了继承。
SubType.protype = new SuperType();
 
let instance = new Subtype(); 

图解:

image.png 这样就组成了一条新的原型链,从而就可以在子类身上访问父类的属性和方法。

缺点:

  • 由于直接修改的原型链,所以父类上的属性和方法在所有子类都是共享的,方法共享时我们期望的,但是属性共享就会有问题。 如果其中一个子类修改了父类的属性,那所有子类访问父类这个属性时,都会被修改,这不是我们期望看到的。
 
function SuperType() { 
 this.colors = ["red", "blue", "green"]; 
} 
 
function SubType() {} 
// 继承 SuperType 
SubType.prototype = new SuperType(); 
 
let instance1 = new SubType(); 
instance1.colors.push("black"); 
console.log(instance1.colors); // "red,blue,green,black" 
 
let instance2 = new SubType(); 
console.log(instance2.colors); // "red,blue,green,black" 

-由于子类和父类没有直接的关系, 所以我们在创建实例时,不能进行传参。

注:原型继承还有个问题,由于原型继承,只是将子类的prototype属性指向了父类的实例,这个父类的实例是没有constructor这个属性的,所以,上述例子中instance.constructor指向的是SuperTye;

盗用构造函数继承

为了解决原型继承的两个缺陷,所以出现了盗用构造函数继承也叫经典继承或者伪继承。

盗用构造函数核心: 就是在子类中执行了父类构造函数,将父类的属性打在子类属性上。

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

这样就解决了每个实例对引用类型属性的修改都会被其他的实例共享的问题。

再看第二个传参问题:既然可以执行父类函数,那当然就可以传参啦。

function SuperType(name){ 
 this.name = name; 
} 
function SubType() { 
 // 继承 SuperType 并传参
 SuperType.call(this, "Nicholas"); 
 // 实例属性
 this.age = 29; 
} 
let instance = new SubType(); 
console.log(instance.name); // "Nicholas"; 
console.log(instance.age); // 29 

缺点:

盗用构造函数使得子类和父类没有直接的关系,也就是原型链断裂。这使得子类无法访问父类的方法,无法进行共享。

每次创建一个实例,就会执行一遍父类的构造函数,都会创建一份副本,当父类构造函数中有大量方法时,就会造成内存浪费。

组合继承

结合了原型继承和盗用函数继承的优点,

使用原型继承的方式继承父类的方法(解决了盗用构造函数中方法不能共享的问题)。

使用盗用构造函数的方式继承父类的属性(解决了原型模式中属性共享的问题)。


function SuperType(name){
    this.name = name;
    this.color = ["red","blue"]
}
//给SuperType构造函数的原型上添加了方法。
SuperType.prototype.sayname = function(){
    
}
 
function SubType(name,age){
    //通过盗用构造函数方法,继承父类的属性
    SuperType.call(this,name);
    this.age = age
}
//使用原型链继承父类的方法,创建子类原型
SubType.prototype = new SuperType();
 
let instance1 = new SubType("Nicholas", 29); 
 
instance1.colors.push("black"); 
console.log(instance1.colors); // "red,blue,green,black" 
instance1.sayName(); // "Nicholas"; 
 
let instance2 = new SubType("Greg", 27); 
//可以发现 实例属性都是独立的,方法可以共享。
console.log(instance2.colors); // "red,blue,green" 
instance1.sayName(); // "Greg"; 

缺点:

效率问题:父类的构造函数执行了两遍,一次创建子类原型是调用,一次是子类构造函数中调用。

原型式继承

一个外国人提出的,核心思想是不使用自定义类型,也能通过原型实现对象之间的信息共享。

function createObj(o) {
    function F(){}    
    F.prototype = o;    
    return new F();
}
var person = {
    name : 'arzh',
    body : ['foot','hand']
}
    
var person1 = createObj(person)
var person2 = createObj(person)
 
console.log(person1) //arzhperson1.body.push('head') 
console.log(person2) //[ 'foot', 'hand', 'head' ]

有小伙伴就会发现,这不就是Object.create(obj)吗? 是的,ES5将这个思想规范化了。

缺点:

因为他的本质是进行了一层浅复制,所以对于属性中的引用类型再实例中是共享的。和原型链模式的问题一样。

寄生式继承

也是上面那个外国人提出的,

核心思想就是:先将对象复制一份,然后再增强这个对象,


function createEnhanceObj(o) {
    //将要继承的对象先复制一份
    var clone = Object.create(o);
    //增强这个对象    
    clone.getName = function () {        
        console.log('arzh')    
    }
    //返回这个对象
    return clone;
}

缺点:

和盗用构造函数一样,父类的方法不能共用。

6.寄生组合式继承

因为组合式继承的效率问题(执行了两遍父类构造函数),所以有了寄生组合式继承。

我们再捋一下执行了两遍父类构造函数的目的。

第一遍创建子类原型,目的是建立子类和父类的原型链,达到父类方法复用的目的。

第二遍是再子类中执行父类的构造函数,目的是父类的属性不共享。

经过上述所说的原型式继承,那就说明可以 不创建父类的实例,而是直接将父类的原型拷贝一份,然后指向子类的原型就可以了。

function inheritPrototype(subType, superType) { 
 //第一步,先创建父类原型的副本
 let prototype = object(superType.prototype); 
 //第二步,将父类的副本设置constructor属性,解决了重写原型导致默认 constructor 丢失的问题
 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); 
};

寄生式组合继承可以算是引用类型继承的最佳模式。

话术总结

为了在面试中表达得更加流畅,我们应该在训练中改善自己的话术,确保表达逻辑清晰、层次分明,这样既能更好地展示自己的知识水平,也能给面试官留下良好的印象

面试官提问:“讲一讲ES5的继承”。 (回答这个问题,说出每种类型的核心以及优缺点即可)

我: "实现继承的目的就是要做到子类要对父类的方法实现共享,同时属性要单一。

第一种是原型链继承的核心时,通过将子类的prototype指向父类的实例,这样就可以在子类中访问到父类的属性和方法,但是原型链继承的问题是,如果父类中的属性是引用类型,其中如果一个子类对父类的属性进行修改,则其他子类访问父类的属性都是被修改后的,第二个缺点是子类不能像父类传参。

第二种是盗用构造函数继承,核心思想是在子类中调用父类的构造函数,并修改this指向,这样就相当于将父类中的属性和方法平铺在子类中,这样子类就会访问父类的属性和方法,他的缺点就是,在子类函数中执行了父类,所以其实子类和父类是没有关系的,原型链也是断裂的。所以方法是没有共享的,每当创建一个对象,就会复制父类的方法,造成内存资源的浪费,他的优点是,由于复制了属性,所以每个子类中的属性都是独立的,互相不影响。

第三种就是寄生组合式继承,他是将上面两种的优点结合在了一起, 使用原型链继承的方式实现方法的共享,使用盗用构造函数的方式,是的属性互相独立。他的缺点就是效率问题,调用了两次父类的构造函数,第一次在创建子类原型时,第二次是在子类中调用。

第四种继承是原型式继承,他的核心思想是,不创建自定义类型,而是创建父类的副本,其实就是Object.create()。他的缺点和原型链模式一样,由于是进行了一次浅复制,所以父类的属性也是共享的。

第五种继承是寄生式继承,他的核心是先创建父类的副本,再进行对他增强。但是他的缺点和构造函数一样,父类的方法不能共享。

第六种继承就是寄生组合式继承,他的核心思想就是修复了组合式继承的缺点,也就是两次调用父类的构造函数,他的改进方式是将之前创建父类实例作为子类原型修改为,直接将父类的原型复制给了子类的原型,从而减少了一次对父类构造函数的调用。寄生式组合继承可以算是引用类型继承的最佳模式。"

面试官提问:“666”