对象继承的方式你都废了么

248 阅读7分钟

对象继承

对象的继承是一个面试非常容易被问到的一个问题,但是却很少有人能够完整通透地将继承讲明白。

什么是继承

继承就是指2个类之间的一种关系;为了实现数据共享,可以通过继承,使子类也具有父类的相关特征和行为(即属性和方法)。

继承原理

在JavaScript中,对象的继承主要是依赖于原型链来实现的。

使用子类中的某个方法或者属性时,会首先在子类的属性和原型中进行寻找,如果没有找到则会按照原型链一级一级往上找。

6种继承方式

这里我们会详细讲述6种继承的方式。

1.原型链继承

实现方式: 原型链继承的本质就是复制,会把子构造函数的原型对象重写为父类的构造函数的原型。

  function FatherFun(name, age) {
    this.name = name;
    this.age = age;
  }
  FatherFun.prototype.getType = () => {
    console.log("我是父构造函数");
  };
  FatherFun.prototype.wealth = ["money", "house"];

  function SonFun(name, age) {
    this.name = name;
    this.age = age;
  }
  SonFun.prototype = FatherFun.prototype;

  const son1 = new SonFun("xm", 18);
  const son2 = new SonFun("xh", 19);

  son1.getType(); // 我是父构造函数

  console.log(son1.wealth.push("knowledge"));
  console.log(son2.wealth); // ['money', 'house', 'knowledge']
  console.log(
    son1.__proto__ === FatherFun.prototype,
    son1.constructor === FatherFun
  ); // true true

image.png

如上图所示,我们发现在son1的实例上,我们能够调用父构造函数原型上的方法了,所以我们的son1继承了父构造函数原型上的方法。

我们也发现了son1的原型变成了父构造函数的原型,原型中的constructor都指向了FatherFun这个构造函数

而且我们无法给超类型的构造函数传递参数。

同时,如果我们通过子构造函数的实例去修改原型上的引用属性时,子类的所有实例也会同步修改

因此,原型链继承虽然实现了一个简单的继承,但是是存在以下一些问题的:

  1. 无法给父类构造函数传递参数
  2. 子类和父类使用的是同一个原型对象(即父类的原型对象),如果原型链中包含引用类型值的时候,这个引用类型会被所有实例共享。
  3. 通过constructor来判断子类类型时,不会等于子类的构造函数。

2.借用构造函数方式

实现方式: 在子类的构造函数内部通过call或者apply调用父类的构造函数,并将this指向为新创建的对象。

  function FatherFun(name, age) {
    this.name = name;
    this.age = age;
    this.wealth = ["money", "house"];
    this.work = function () {
      console.log("FatherFun构造函数内部方法");
    };
  }
  FatherFun.prototype.sayHi = function () {
    console.log("这是Father的原型里的sayHi");
  };

  function SonFun(name, age) {
    FatherFun.call(this, name, age); //继承了Father,且向父类型传递参数
  }

  const son1 = new SonFun("小明", 18);
  const son2 = new SonFun("弟弟", 3);
  console.log(son1); // age:18 name:小明 wealth:[money,house] work:f()
  console.log(son1.__proto__ === FatherFun.prototype); // false
  //   son1.sayHi(); // sayHi is not a function
  son2.wealth.push("milk");
  console.log(son1.constructor === SonFun); // true
  console.log(son2.wealth, son1.wealth); // ['money', 'house', 'milk'] ['money', 'house']
  console.log(son1 instanceof FatherFun); // false

image.png 如上图所示,我们可以看到通过该方式创建的2个子实例,继承了父构造函数内部的方法和属性。但是并没有继承父构造函数的原型上的方法和属性

当然,相比于第一种原型链的继承,该方式不会有引用类型的问题,同时也能够通过constructor来确定实例的类型。但是通过instanceof来判断时,无法证明son1属于FatherFun的子实例

因此会存在以下一些问题:

  1. 方法都在构造函数中定义
  2. 超类原型对象定义得到方法对子类的实例不可见,故实现不了方法的共享

3.组合继承方式

实现方式: 结合上述的两种继承方式,使用原型链实现对原型属性和方法的继承,通过借用构造函数来实现对实例属性的继承。

  function FatherFun(name, age) {
    this.name = name;
    this.age = age;
    this.wealth = ["money", "house"];
  }
  FatherFun.prototype.getSay = function () {
    console.log("父构造函数原型的getSay方法");
  };

  function SonFun(sex, nickname, name, age) {
    this.sex = sex;
    this.nickname = nickname;
    FatherFun.call(this, name, age);
  }
  SonFun.prototype = new FatherFun();

  const son1 = new SonFun("男", "11", "张三", 16);
  const son2 = new SonFun("女", "33", "lili", 3);
  
  console.log(son1);
  
  console.log(
    son1.__proto__ === SonFun.prototype,
    son1.__proto__ === FatherFun.prototype
  ); // true false
  
  console.log(son1.constructor === SonFun); // false
  
  console.log(son1 instanceof FatherFun); // true
  
  son2.wealth.push("milk");
  console.log(son1.wealth); //  ['money', 'house']
  
  son1.getSay(); // 父构造函数原型的getSay方法

image.png

如上图所示,我们发现通过该方式创建的子实例中,可以通过原型链访问到父构造函数的原型方法,也能够拥有父构造函数的属性。构造函数内的引用类型也不会影响到其他的子实例。

但是我们能够发现一个问题,那就是子构造函数的原型是父构造函数的一个实例,因此son1.constructor === SonFun会等于false。

而且,看上述代码,我们分别在子构造函数内部调用了一次父构造函数,还在外部设置子构造函数的原型时又调用了一次

因此存在以下问题:

  1. 会调用父构造函数2次
  2. 子实例的原型是父构造函数的实例,原型上的constructor属性指向了父构造函数。

4.原型式继承

实现方式: 原型式继承本质是一个浅拷贝,在函数内部先创建一个临时的构造函数,然后将传入函数的对象作为这个函数的原型,最后返回这个函数的实例。

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

  const a = {
    a: 1,
    b: 2,
    say: function () {
      console.log("这是a的say方法");
    },
  };
  const result = object(a);
  console.log(result);
  console.log(result.a); // 1
  result.say(); // 这是a的say方法

image.png 如上图所示,得到的是一个F()的实例,而F构造函数的原型是指向传入object(o)的对象a。因此我们在后面使用result.aresult.say时,实际上是通过原型链访问到了F构造函数的原型。

在es6中,Object.create()实现了这种继承方式。

5.寄生式继承

实现方式: 寄生式继承的实现方式就是在原型式继承的基础上,通过某种方式来增加对象。

  function object(o) {
    function F() {}
    F.prototype = o;
    return new F();
  }
  function createAnother(original) {
    var clone = object(original); //通过调用object函数创建一个新对象
    clone.sayHi = function () {
      //以某种方式来增强这个对象
      alert("hi");
    };
    return clone; //返回这个对象
  }
  const o = {
    a: 1,
    b: 2,
  };
  const result = createAnother(o);
  console.log(result);

image.png

如上述代码所示,寄生式继承就是在原型式继承的基础上,将原型式继承产生的实例进行加强,然后返回这个对象。

6.寄生式组合继承

实现方式: 可以理解为寄生式继承和组合式继承的结合版本。

  1. 首先一个极限继承的核心函数。
  2. 在函数内部来修改子类的原型。
    1. 这个函数有2个参数,分别是子构造函数和父构造函数。
    2. 定义一个临时构造函数F,将这个构造函数的原型指向父构造函数的原型。
    3. 然后将子构造函数的原型指向临时构造函数F的实例。
    4. 注意记得将修改过后的子构造函数的原型的constructor属性指回自己。
  3. 子构造函数的内部需要使用call去调用父构造函数,将this指向新创建的对象(和上述构造函数方式实现继承一样)。
  // superType:父构造函数  subType:子构造函数
  function inheritPrototype(subType, superType) {
    function F() {}
    //F()的原型指向的是superType
    F.prototype = superType.prototype;
    //subType的原型指向的是F()
    subType.prototype = new F();
    // 重新将构造函数指向自己,修正构造函数
    subType.prototype.constructor = subType;
  }
  // 设置父类
  function SuperType(name) {
    this.name = name;
    this.colors = ["red", "blue", "green"];
    SuperType.prototype.sayName = function () {
      console.log(this.name);
    };
  }
  // 设置子类
  function SubType(name, age) {
    //构造函数式继承--子类构造函数中执行父类构造函数
    SuperType.call(this, name);
    this.age = age;
  }
  // 核心:因为是对父类原型的复制,所以不包含父类的构造函数,也就不会调用两次父类的构造函数造成浪费
  inheritPrototype(SubType, SuperType);
  // 添加子类私有方法
  SubType.prototype.sayAge = function () {
    console.log(this.age);
  };
  var instance = new SubType("Taec", 18);
  console.log(instance);
  console.log(instance.__proto__);
  console.log(instance.constructor);

image.png

如上述所示,这种方式是一个比较推荐的继承方式。

  1. 能够将父类的方法进行复用
  2. 也不会和组合式继承一样调用两次父类构造函数
  3. 子类构建实例的时候也能往父类传递参数