JS继承

112 阅读6分钟

js继承的几种方式

  1. 盗用构造函数(经典继承)
  2. 组合继承
  3. 原型继承
  4. 寄生式继承
  5. 寄生式组合继承

接下来,博主会结合自己所学到的,理解到的,详细的讲出来。一起来看看吧!

回顾

在这之前我们就要简单了解一下原型链和继承的关系了,请看下面这个例子:

      function Person() {
        this.name = "丁时一";
      }
      Person.prototype.say = function () {
        console.log(this.name);
      };
      //子类
      function Robot() {
        this.height = 20;
      }
      //继承Person ,将父类的实例作为prototype从而根据原型链可以访问父类的属性和prototype
      Robot.prototype = new Person();

      Robot.prototype.move = function () {
        console.log(this.height);
      };
      const robot = new Robot();
      robot.say(); //丁时一
      robot.move(); //20

在这个例子中,使用父类的实例作为子类的prototype来共享父类的属性和prototype上的属性。但是这也存在着许多问题,其中之一就是引用值了,来我们接着看。

      function Person() {
        this.color = ["blue", "red", "green"];
      }
      Person.prototype.say = function () {
        console.log(this.color);
      };
      //子类
      function Robot() {}
      //继承Person ,将父类的实例作为prototype从而根据原型链可以访问父类的属性和prototype
      Robot.prototype = new Person();
      const robot = new Robot();
      const robot2 = new Robot();
      console.log(robot.color); //["blue", "red", "green"]
      robot.color.push("black");
      //另一个实例
      console.log(robot2.color); //["blue", "red", "green","black"]

我们在父类上定义了一个属性color,并且使用之前的继承方式,可以看到的是,当我们修改一个实例的color属性之后(修改构造函数的prototype),这个变化也会在其他实例上反映出来。(在原型链最后一个点说的是原型的动态性)。这就是引用值

好了,简单了解了一下引用值的问题,那么我们就步入正题吧!

盗用构造函数(经典继承)

经典继承解决了之前所提到的引用值的问题,让每一个实例都拥有自己的属性,而不是通过父类的prototype去共享属性。基本的实现思路:在子类构造函数中调用父类构造函数。怎么样,get到了吗1没有的话没关系,来看一个例子:

      function Person() {
        this.color = ["red", "green", "black"];
      }
      function Robot(name) {
        Person.call(this);
        this.name = name;
      }

      const robot1 = new Robot("机器人1");
      const robot2 = new Robot("机器人2");
      console.log(robot1.color === robot2.color); //false
      robot1.color.push("black");
      console.log(robot1.color); //["red", "green", "black", "black"]
      console.log(robot2.color); //["red", "green", "black"]

在这之前,博主有在前面的文章讲到了new的过程发生了什么,如果不了解的朋友可以先了解一下,如果了解了的,就跟着往下看吧。

在使用子类构造函数创建实例的时候,将Person中的上下文替换成为子类中的this并添加color属性,根据之前所提到的new的时候创建一个新对象,把这个新对象的__proto__指向构造函数的prototype,并且把this指向这个对象,然后添加对象并返回这个对象。,这样一来,每个实例都是独立的属性,互不干扰了。

这种方法可以在子类构造函数中向父类构造函数传递参数。来看下一个例子:

      function Person(owner) {
        this.owner = owner;
      }
      function Robot(owner) {
        //传递参数
        Person.call(this, owner);
        this.name = "机器人";
      }

      const robot = new Robot("丁时一");
      console.log(robot.owner);//丁时一
      console.log(robot.name);//机器人

传递的参数,会在使用call改变Person上下文并调用Person的时候传递给person(也可以使用apply)。

缺点:经典继承的缺点在于方法也需要在构造函数中定义,此外,子类也不能访问定义在父类原型上定义的方法,因此所有的类型都只能使用构造函数模式。由于存在这些问题,京单继承基本上也不能单独使用。

组合继承

将原型链和经典继承结合了起来,非常的巧妙。基本思路:使用原型链继承父类原型上的方法,使用经典继承来继承实例属性,这样就既可以把方法定义在原型上实现重用,又可以让每个实例都有自己的属性。看下一个例子:

      function Person() {
        this.name = "人类";
      }
      Person.prototype.run = function () {
        console.log("人类跑步");
      };

      function Robot(name) {
        //通过经典继承的方式继承父类实例的属性
        Person.call(this);
        this.name = name;
      }

      //通过原型链来继承原型上的方法和属性
      Robot.prototype = new Person();

      const robot = new Robot("机器人");
      robot.run(); //人类跑步
      console.log(robot.name); //机器人
      console.log(robot.__proto__.name);//人类

因为使用父类实例作为子类构造函数的prototype,所以父类实例能够访问到的,prototype都能做到,通过原型链,子类实例也能访问到。并且如果属性名相同的话,会遮蔽原型上的属性。

组合继承弥补了原型链和经典继承的不足,是JavaScript中使用最多的继承的方式,而且继承也保留了instanceofisPropertypeOf识别合成对象的能力。(A instanceof B检测 B的原型是否出现在A的原型链中。

原型式继承

之前的两种方式都是通过自定义类型,创建实例共享信息的,那么这种方式就是不用自定义类型也可以通过原型实现对象之间的信息共享。来看下面这个函数:

      //p为需要共享的对象信息
      function object(p) {
        function F() {}
        F.prototype = p;
        return new F();
      }

      const info = {
        age: 20,
        height: 170,
        color: ["red"],
      };

      const person1 = object(info);
      const person2 = object(info);

      person2.color.push("black");
      console.log(person2.color); //["red", "black"]
      console.log(person1.color); //["red", "black"]

在这个例子中,object函数实际上式将传入的对象做了一个浅赋值。

ECMAScript5通过增加Object.create()方法将原型式继承的概念规范化了。下面的例子和上面的object函数效果相同。

      let person = {
        color: ["red", "green"],
      };

      const p1=Object.create(person)
      const p2=Object.create(person)
      p1.color.push("black")
      console.log(p1.color);
      console.log(p2.color);

寄生式继承

于原型继承比较靠近的一种继承方式是寄生式继承。寄生式继承的思路类似于寄生构造函数和工厂模式(创建对象的方法)。创建实现一个构造函数,以某种方法增强一个对象,然后返回这个对象。来看下面这个例子:

      function other(original) {
        const clone = Object.create(original);
        clone.say = function () {
          console.log("say Hi");
        };
        return clone;
      }

      const info = {
        color: ["red"],
      };

      const p = other(info);
      p.say();//say Hi

总结:寄生式继承和原型继承都适合于主要关注对象,不在乎类型和构造函数场景。

寄生式组合继承

之前的组合继承其实也存在效率问题,最主要的就是父类构造函数始终会调用两次(一次call,一次实例赋值给子类原型)。本质上,子类原型最终是要包含父类对象所有实例属性,子类构造函数只要在执行时候重写原型就好了。

      function usefulFunction(Father, Son) {
        let prototype = Father.prototype;
        prototype.constructor = Son;
        Son.prototype = prototype;
      }
      function Person(name) {
        this.name = name;
        this.color = ["red"];
      }
      Person.prototype.say = function () {
        console.log(this.name);
      };

      function Robot(name, age) {
        Person.call(this, name);
        this.age = age;
      }
      usefulFunction(Person, Robot);

      Robot.prototype.sayAge = function () {
        console.log(this.age);
      };

      const robot = new Robot("丁时一", 20);
      robot.say(); //丁时一
      robot.sayAge(); //20

在本例子中,我们将之前使用父类实例作为子类的原型给去掉了,然后直接将父类原型作为子类构造函数原型,因为是直接覆盖的原因(之前在讲constructor的时候有讲到过),所以需要把constructor重新指回子类构造函数。这样就代替了之前的Robot.prototype=new Person()啦!并且父类构造函数也只执行了一次。而且原型键仍保持不变。寄生式组合继承可以算是引用类型继承的最佳方法了。

今天就到这里啦,拜了个拜~