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原型上查找找到了,所以就打印出父亲开始吃饭通过这种方式继承,只要父类新增公共属性或公共方法或原型属性和方法,子类都可以访问到,但不能给父类构造传递参数,这在原型链中是标准做法。要确保构造函数没有任何参数,也无法实现多继承。

其实上面的解释多少有点牵强,为什么不能给父类构造传递参数,那接下来我们就通过一个例子来具体剖析下它的内部原理

    function Father(name) {
      this.name = name;
      this.obj = {
        work:'IT',
        city:'BJ'
      }
    }
    Father.prototype = {
      constructor: Father,
      PI: 3.14,
    };
    function Son(age) {
      this.age = age;
    }
    Son.prototype = new Father("父亲");
    Son.prototype.constructor = Son;

    const son1 = new Son(10);
    son1.name = "杰克";
    son1.obj.city = "USA";
    const son2 = new Son(20);
    son2.name = "萝丝";
    son2.obj.city = "China";

    son2.obj.sex = 'woman'; //单独给son2实例添加

    console.log(son1.name); //杰克
    console.log(son1.obj.city); //China

    console.log(son2.name); //萝丝
    console.log(son2.obj.city); //China

    console.log(son1.obj.sex); //woman
    console.log(son2.obj.sex); //woman

    console.log(son1);
    console.log(son2);

为什么不能给父类构造函数传递参数?
因为父类的构造函数是在我们写 Son.prototype = new Father(“父亲”) 时执行的,而且只执行一次,当我们创建子类实例时如 const son1 = new Son(10) ,父类构造函数是不执行的,所以父类构造函数中的属性就是所有子类实例所共享的属性,其实这也是原型的作用。正是由于在创建子类实例时父类构造函数并不会执行,所以我们没有必要给父类构造函数传递参数,直接在父类构造函数中定义就可以了。

为什么son1.name 和 son2.name 值不一样?

这个问题就牵扯出了属性的原型链查找和属性赋值的问题了

  1. 当我们进行属性的查找时是基于原型链查找的
  2. 当我们给属性赋值时,也会先通过原型链查找是否存在此属性,如果存在并且属性的 writable 为true,那也不会修改原型链上的属性值,而是在自己实例上创建新属性,因为原型链提供的是公共的属性和方法,如果每个实例都可以修改原型链上的属性,那原型对象将很难维护。所以当原型链上属性值为基本数据类型时(如:String,Number),这些值是不能被修改的,只会在自己的实例上修改或创建属性
  3. 如果原型上存在该属性但 writable 为 false,那么不管是原型还是子类实例都不能给该属性赋值
  4. 如果在原型上也没查找到该属性,则也会在子类实例上创建该属性

为什么son1.obj.city 和 son2.obj.city值一样呢?
看完了上面的解释,你应该很快就会产生这个疑问,这又是为什么呢?
其实还是那句话js属性是基于原型链进行查找的,而我们通过 . (点语法) 就是在查找属性,而通过 =(等号) 是在给属性赋值。

  1. 当执行 son1.obj.city = “USA” 时,son1.obj先找到了原型上的obj对象,然后就基于obj对象开始了属性赋值,在赋值时就遵循了上个问题的流程
  2. 当执行 son2.obj.city = “China” 时,son2.obj也是先找到了原型上的obj对象,这个obj对象和son1.obj对象是同一个对象,在赋值时就会覆盖之前的值
  3. 当执行 son2.obj.sex = “woman” 时,son2.obj也是先找到了原型上的obj对象,然后发现obj对象中没有sex属性,则会创建sex属性并赋值为woman

所以当原型链上的属性值为Object、Array等引用类型时,那么当多个实例修改时,其实修改就是同一个对象。所以就有了上面的打印结果
son1和son2对象 如下图
在这里插入图片描述

构造继承

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

    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(); //父亲开始玩游戏

通过这种继承的方式,即可以向父类中传递参数,因为调用call方法就会调用父类的构造函数,也不会创建多余的实例占用内存,也可以通过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;
  • 19
    点赞
  • 79
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值