JS中继承的几种方式

原型链继承

核心:将父类的实例作为子类的原型

function Father() {
        this.name = "父亲";
        this.run = function () {
            console.log(this.name + "开始跑步")
        }
    }

    Father.prototype = {
        constructor: Father, //自定义原型对象时,尽量重写constructor再指向该函数
        hei: 200,
        eat: function () {
            console.log(this.name + "开始吃饭")
        }
    };
    
    function Son(age) {
         this.age = age;
         this.sleep = function () {
             console.log(this.name + "开始睡觉")
         }
     }

     Son.prototype = new Father();
     Son.prototype.constructor = Son;
     Son.prototype.playGame = function () {
         console.log(this.name + "开始玩游戏")
     };
     const son = new Son(10);

     console.log(son.name); //父亲
     console.log(son.age); //10
     son.run(); //父亲开始跑步
     son.eat(); //父亲开始吃饭
     son.sleep(); //父亲开始睡觉
     son.playGame(); //父亲开始玩游戏

基于原型链继承的查找方式是,如son.run()首先现在son对象内查找,没有找到则通过son.__proto__去Son.prototype 原型链上查找,所以就进入到new Father()对象中查找,找到了所以就打印出父亲开始跑步,查找son.name是同样的步骤,当查找son.eat()时,在new Father()对象中没有找到,则又通过Father.__proto__去Father.prototype原型上查找找到了,所以就打印出父亲开始吃饭通过这种方式继承,只要父类新增公共属性或公共方法或原型属性和方法,子类都可以访问到,但不能给父类构造传递参数,这在原型链中是标准做法。要确保构造函数没有任何参数,也无法实现多继承,而且当为子类新增原型方法或属性时,也必须要在Son.prototype = new Father()之后,否则会被覆盖,
虽然这种方式可以继承,但不符合语义,因为Son.prototype指向了一个具体的实例对象,如指向了一个 具体的人。

构造继承

核心:复制父类的实例属性和方法给子类对象,但不包括父类构造的原型对象

    function Son(age) {
        Father.call(this);
        this.age = age;
        this.sleep = function () {
            console.log(this.name + "开始睡觉")
        }
    }

    Son.prototype.playGame = function () {
        console.log(this.name + "开始玩游戏")
    };
    const son = new Son(10);

    console.log(son.name); //父亲
    console.log(son.age); //10
    console.log(son.hei); //undefined 因为无法调用Father原型上的属性
    son.run(); //父亲开始跑步
    // son.eat(); //会报错 因为无法调用Father原型上的方法
    son.sleep(); //父亲开始睡觉
    son.playGame(); //父亲开始玩游戏

通过在Son构造函数中调用Father.call(this)去改变Father构造中this的指向来达到继承,
这种通过call方式实现的继承,即可以实现多继承,也解决了不能向Father中传递参数的问题,但是却无法继承Father原型上的属性和方法,而且通过call方式的实现的继承,相当于给每个对象添加了父类构造中的属性和方法无法实现方法的复用,也占用堆内存空间。

拷贝继承

核心:通过for…in,把父类对象和原型对象上可枚举的属性和方法循环赋值到Son的原型上

    function Son(age) {
        var father = new Father();
        for (let i in father) {
            Son.prototype[i] = father[i]
        }
        this.age = age;
        this.sleep = function () {
            console.log(this.name + "开始睡觉")
        }
    }

    Son.prototype.playGame = function () {
        console.log(this.name + "开始玩游戏")
    };
    const son = new Son(10);

    console.log(son.name); //父亲
    console.log(son.age); //10
    console.log(son.hei); //200
    son.run(); //父亲开始跑步
    son.eat(); //父亲开始吃饭
    son.sleep(); //父亲开始睡觉
    son.playGame(); //父亲开始玩游戏

这种方式虽然可多继承、可向父类传参,但需要循环遍历父类属性和方法效率低,而且父类原型的属性和方法也无法复用,占用堆内存空间。

组合继承 原型链+call继承

  function Son(age) {
        Father.call(this);
        this.age = age;
        this.sleep = function () {
            console.log(this.name + "开始睡觉")
        };
    }

    Son.prototype = new Father();
    // Son.prototype = new Father().__proto__;
    // Son.prototype = Father.prototype;
    //让constructor再指向Son构造函数,否则会执行Father构造函数
    Son.prototype.constructor = Son;
    Son.prototype.playGame = function () {
        console.log(this.name + "开始玩游戏")
    };
    const son = new Son(10);

    console.log(son.name); //父亲
    console.log(son.age); //10
    console.log(son.hei); //200
    son.run(); //父亲开始跑步
    son.eat(); //父亲开始吃饭
    son.sleep(); //父亲开始睡觉
    son.playGame(); //父亲开始玩游戏

通过这种组合的模式,实现了向父类构造传递参数,也可实现父类构造函数内属性和方法的多继承,但无法实现父类原型的多继承, 通过Son.prototype = new Father()只可让子类的原型继承某一父类的原型,而且组合继承会调用两次 父类构造函数,只是子类实例中的属性和方法把子类原型上的屏蔽掉了而已。同样会占用内存。

注意:
刚开始接触原型的时候,我就在想是不是可以这样写Son.prototype = new Father().proto,这不就是我们需要的继承方式吗?还省了内部直接往上查找了,或者更直接点Son.prototype = Father.prototype,这样父类构造函数也只会调用一次,这不是最完美的吗,其实不是的,这牵扯到对象引用的问题,也就是说如果我们给Son.prototype对象上添加一个属性或方法,那么也就会给Father.prototype对象上添加,因为它们指向的是同一个对象,所以我们必须使用Son.prototype =Object.create(Father.prototype)这种方式,这样才能区分两个对象的引用。

对象冒充继承

function Father(name) {
        this.name = name;
        this.run = function () {
            console.log(this.name + "开始跑步")
        }
    }

    function Son(name) {
        this.newMethod = Father;
        this.newMethod(name);
        delete this.newMethod;
    }

    const son = new Son("父亲");
    console.log(son); //Son {name: "父亲", run: ƒ}
    console.log(son.name); //父亲
    son.run(); //父亲开始跑步

使用对象冒充的方式实现继承,只能集成父类构造函数中带有this的属性和方法,不能继承父类原型上的方法想要理解对象冒充继承就必须要理解this指向的问题,我们在Son构造函数中,通过this.newMethod = Father;给Son自定义了一个属性字段newMethod并把Father函数赋值给它(此时Father就当一个函数来用),然后我们通过newMethod调用Father这个函数,并把name值传递过去,此时Father函数中的this指向的是通过new创建的对象son,这就相当于给对象son添加了Father中带this的属性或方法,然后再通过delete this.newMethod把Son构造函数中临时添加的属性删除掉。这样就实现了子类继承父类的属性和方法了。同时还支持多继承,但当多继承出现同名属性或方式时,后面的会覆盖前面的。

寄生组合继承

    function Son(age) {
        Father.call(this);
        this.age = age;
        this.sleep = function () {
            console.log(this.name + "开始睡觉")
        }
    }

    Son.prototype = Object.create(Father.prototype);
    Son.prototype.constructor = Son;
    Son.prototype.playGame = function () {
        console.log(this.name + "开始玩游戏")
    };
    const son = new Son(10);

    console.log(son.name); //父亲
    console.log(son.age); //10
    son.run(); //父亲开始跑步
    son.eat(); //父亲开始吃饭
    son.sleep(); //父亲开始睡觉
    son.playGame(); //父亲开始玩游戏

通过这种继承的方式,即可以向父类中传递参数,也不会创建多余的实例占用内存,也可以通过Object.assign实现多继承,推荐使用

Object.assign实现原型上的多继承

Object.assign() 方法用于将所有可枚举属性的值从一个或多个源对象复制到目标对象。它将返回目标对象
如果目标对象中的属性具有相同的键,则属性将被源对象中的属性覆盖。后面的源对象的属性将类似地覆盖前面的源对象的属性

  • 通过call实现对象上的多继承
  • 通过Object.assign把其他类原型上的属性和方法拷贝到Son的原型上,以此实现原型上的多继承
  function Son() {
    Father.call(this);
    OhterClass1.call(this);
    OhterClass2.call(this);
  }

  Son.prototype = Object.create(Father.prototype);
  Object.assign(Son.prototype, OhterClass1.prototype, OhterClass2.prototype);
  Son.prototype.constructor = Son;

你可能感兴趣的:(前端)