JS中继承的几种方式?

176 阅读4分钟

前言

js中继承是一种编程概念,子类可以继承父类的属性和方法,说到了继承又不得不提及一个概念叫多态:也就是相同的方法,不同的表现形态,子类可以重写父类的方法。

继承的方式

  1. ES5继承,利用构造函数,原型和原型链(原型链继承,借用构造函数继承,组合继承,原型式继承,寄生式继承,寄生组合式继承)。
  2. ES6继承,class继承,extends关键字实现继承(写法上清晰直观),ES6规定,子类必须在constructor中调用super(),子类没有自己的this,调用super(),等同于调用父类的构造函数,实例化出一个子对象,子类就得到了自己的this对象,拥有与父类相同的属性和方法实现继承。
  • 区别:
    • ES5继承:先创建一个独立的子类实例对象,然后再将父类的属性和方法添加上去,也就是“实例在前,继承在后”
    • ES6继承:先将父类的属性和方法添加到一个空对象上面,然后再将该对象作为子类的实例,也就是“继承在前,实例在后”

继承的简单实现和各自优缺点

原型链继承

  • 构造函数:可以通过new来新建一个对象的函数
  • 原型:通俗点讲就是一个js对象,用来放置一些公共的属性和方法
  • 实例:通过构造函数实例化出来的对象 三者之前的关系:每个构造函数都有个有原型对象,每个实例对象都有__porto__属性指向构造函数的原型,构造函数的原型对象同样也有__proto__属性,也指向对应的构造函数的原型,这种有__proto__连接着原型组成的链式结构,就形成了原型链

image.png

   function Parent(name){
       this.parentName = 'parent-name';
   }
   Parent.prototype.getParentName = function(){
       console.log(this.parentName);
   }
   function Child(){
       this.name = 'child-name'
   }
   Child.prototype = new Parent();
   Child.prototype.constructor = Child;
   Child.prototype.getChildName = function(){
       console.log(this.name);
   }
   let p1 = new Child();
   p1.getParentName() // parent-name
   p1.getChildName() // child-name

上述例子,是原型链式继承,本质是重写原型对象

  • 缺点:
  1. 公用同一个原型对象,也就是原型的引用地址相同,如果给原型Parent增加一些属性和方法,会导致所有的子实例的属性和方法都发生改变
function Parent(name) {
	this.parentName = "parent-name";
	this.color = [1, 2];
}
Parent.prototype.getParentName = function () {
	console.log(this.color);
};
function Child() {
	this.name = "child-name";
}
Child.prototype = new Parent();
let p1 = new Child();

p1.getParentName(); // 【1,2】
let p2 = new Child();
p2.color.push(3);
p2.getParentName(); // 【1,2,3】
p1.getParentName(); // 【1,2,3】
  1. 没法向构造函数中传递参数

借用构造函数继承

在子类中调用父类的构造函数,俗称对象冒充

function Parent(){
    this.color = [1,2];
}
function Child(){
    Parent.call(this)
}
let p2 = new Child();
p2.color.push(3);
console.log(p2.color); // [1,2,3]
let p1 = new Child();
console.log(p1.color); // [1,2]
let p3 = new Parent();
console.log(p3.color); // [1,2]
  • 缺点:
  1. 无法继承父类原型上的属性和方法,此方法可以传递参数
function Parent(name){
    this.color = [1,2];
}
function Child(){
    Parent.call(this,name)
}

组合继承

组合继承是原型链继承和借用构造函数的结合体,其实现思想是利用原型链实现对原型属性和方法的继承,而通过构造函数,实现对实例属性的继承。

function Parent(name){
    this.name = name;
    this.color = [1,2]
}
Parent.prototype.sayName = function(){
    console.log(this.name);
    console.log(this.color);
}
function Child(name){
    Parent.call(this,name)
}
Child.prototype = new Parent();
Child.prototype.constructor = Child;
let p1 = new Child('ljz');
p1.sayName(); // 'ljz'  [1,2]
p1.color.push(3);
p1.sayName(); // 'ljz'  [1,2,3]
let p2 = new Child('ljz1');
p2.sayName(); // 'ljz1'  [1,2]

缺点:会调用两次父类构造函数,它是JavaScript中最常用的继承模式

原型式继承

原型式继承和原型链继承有点相似,不过他的思想主要是借助原型基于已有有的对象创建新对象,同时还不必创建自定义类型

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

其本质就是Object.create(obj,options),options可以重写obj里面的一些属性

  • 缺点: 引用类型的属性值都会始终共享,和原型链模式一致

寄生式继承

寄生式继承与原型式继承紧密相关,只不过对原型式继承返回对象,做了一些增强,就是创建一个仅用于继承过程的函数,改函数在内部一某种方式增强该对象,

    function createEnhanceObj(o){
        let clone = createObj(o);
        clone.sayHi = function(){
            console.log('增强了');
        }
        return clone;
    }
  • 缺点: 使用寄生式继承添加函数,函数复用性差,从而会降低效率

寄生组合式继承

基于组合继承的缺点,思考问题,缺点是调用了两次父类的构造函数,这样会使,子实例的原型,和自身属性上存在一些重复,构造函数继承缺点是没法继承父类原型上的属性和方法,那么我们只需要生成一个父类原型的副本,为副本添加constructor属性指向子类构造函数,最后将这个副本赋值给子类构造函数的原型即可。

    function Parent(name){
        this.name = name;
    }
    Parent.prototype.getParentName = function(){
        console.log('parent');
    }
    function Child(name){
        this.name = name;
    }
    function enhanceChild(parent,child){
        const prototype = Object.create(parent.prototype);
        prototype.constructor = child;
        child.prototype = prototype;
    }
    enhanceChild(Parent,Child)
    Child.prototype.getChildName = function(){
        console.log('child')
    }
    let p1 = new Child();
    p1.getParentName(); // parent
    p1.getChildName(); // child

总结

ES5的继承都是围绕着原型,原型链实现继承,多种继承方式各有优缺点,请结合实际场景使用。