JS继承的几种方式

3,274 阅读4分钟

我们先写个父类和子类

    function Parent(){
        this.name = {name:'jicheng'};
    }
    Parent.prototype.pro = function(){
        console.log('prototype');
    }
    function Child(){};

一、原型链继承

  • 核心:把父类私有+公有的属性,都作为子类公有
    Child.prototype = new Parent();//父类的实例作为子类的原型
    Child.prototype.constructor = Child;//手动指定constructor指向
    let c = new Child();
    console.log(c.name,c.pro())
  • 子类的实例通过__proto__ 找到所属类Child的原型prototype,即父类的一个实例 该实例拥有Parent的私有属性--实例上的属性,
  • 特点:
    • 简单、易实现
    • 实例既是子类的实例,也是父类的实例
    • 父类的公私属性都能拿到。
    • 无法实现多继承,不能向父类传参

二、call继承 (构造继承)

  • 核心:把父类私有作为子类私有

    通过使用call、apply方法可以在新创建的对象上执行构造函数,用父类的构造函数来增加子类的实例,等于是复制父类的实例属性给子类(没用到原型)

function Child(){
    Parent.call(this);
}
  • 特点:
    • 简单明了
    • 实例只是子类的实例,并不是父类的实例
    • 可以实现多继承,可以传参
    • 不能继承原型上的属性
    • 每个子类都有父类函数的副本,影响性能

三、实例继承

  • 核心:把父类公有和私有属性作为子类私有 在子类中返回父类的实例
    function Child(name){
        var p = new Parent();
        return p;
    }
  • 特点:
    • 实例是父类的实例,不是子类的实例
    • 不支持多继承

四、拷贝继承

  • 核心:把父类公有和私有属性作为子类公有

    在子类中遍历父类的实例,然后分别赋值给子类prototype

    function Child(name){
        var p = new Parent();
        for(let key in p){//for in 可以把p的__proto__上的属性也可以遍历到
            Child.prototype[key] = p[key]
        }
    }
  • 特点:
    • 效率低,占内存高
    • 可以实现多继承
    • 无法继承父类不可枚举的方法(for in)

五、组合继承

  • 核心:原型继承+构造继承,把父类私有作为子类私有,父类公有作为子类公有

    在子类中添加父类的实例并改变this指向,然后把父类的实例赋值给子类的原型 注意恢复子类Child的原型prototype的constructor指向

    function Child(name){
        Parent.call(this);
    }
    Child.prototype = new Parent();//实际上把父类私有也带过来了,但是子类实例访问的时候首先访问子类的私有属性
    Child.prototype.constructor = Child
  • 特点:
    • 弥补了构造继承的缺陷,可以继承实例属性/方法,也可以继承原型属性/方法
    • 既是子类的实例,也是父类的实例
    • 不存在引用属性共享问题
    • 可传参 函数可复用
    • 调用了两次父类构造函数,生成了两份实例(子类实例将子类原型上的那份屏蔽了)

六、寄生组合继承

  • 核心:把父类私有作为子类私有,父类公有作为子类公有

    在子类中添加父类的实例并改变this指向

    //实例属性
    function Child(name){
        Parent.call(this);
    }
    //公有属性
    (function(){
        let M = function(){};
        M.prototype = Parent.prototype;
        Child.prototype = new M();
        Child.prototype.constructor = Child;
    })()
    其实上述共有属性的继承方式也就是模仿Object.create()的原理
    所以也可以写成:
    Child.prototype = Object.create(Parent.prototype,{constructor:{value:Child}})
  • 特点:完美

七、类的继承

  • 核心:父类公有作为子类公有,父类私有作为子类私有

    es6方法extends,以及属性constructor super等

    class Child extends Parent{
        constructor(){
            //子类必须在constructor方法中调用super方法,否则新建实例时会报错,
            //因为子类没有自己的this对象,而是继承父类的this对象,然后对其进行加工。如果不调用super方法,子类就得不到this对象。
            super()
        }
    }
  • 特点:
    • 内部super()相当于Parent.call(this)
    • 外部extends相当于Object.create(Child.prototype,Parent.prototype,{constructor:{value:Child}})
    • 可以传参
  • 补充:
    • __prpto__本质上是一个内部属性,而不是一个正式的对外的 API,目前,所有浏览器(包括 IE11 )都部署了这个属性。
    • Child.prototype=Object.create(Parent.prototype,{constructor:{value:Child}})是es5方法,原理是创建一个超类接收父类原型上的属性方法,最后返回实例,注意在第二个参数描述器中设置constructor指向
    • Object.setPrototypeOf(Child.prototype, Parent.prototype) 是ES6正式推荐的设置原型对象的方法。

总结

  • 原型继承:父类公+私=>子类公有 子类的原型指向父类的实例
    • Child.prorotype = new Parent()
    • Child.prototype.constructor = Child
  • 实例继承:父类公+私=>子类私有
    • function Child(){ return new Parent()} 子类函数内返回父类的实例
  • 构造继承:父类私有=>子类私有
    • function Child(){ Parent.call(this)} 子类函数内执行父类函数
  • 拷贝继承:父类公+私=>子类公有 遍历父类函数,逐个赋值给子类原型
    • for(let key in Parent){ Child.prototype[key] = Parent[key]}
    • Child.prototype.constructor = Child
  • 组合继承 构造+原型
  • 寄生组合继承 构造+Object.create
  • 类的继承 extends + super

实际应用中还是要根据不同的应用场景选择不同的继承方式

欢迎批评指正!