对象以及继承的总结(三)继承

什么是对象?

对象是一组属性的无序集合。每一个属性或者方法都是由一个名称标识来映射一个值。可以把对象想成一张散列表,内容就是一堆的名/值对。

对象的继承

实现继承是ECMAScript唯一支持的继承方式,这主要是通过原型链实现的。

原型链实现继承

原型链是主要的继承方式,思想是:通过原型继承多个引用类型的属性和方法

基本构思:原型本身有一个内部指针指向另一个原型,相应的另一个原型也有一个指针指向另一个构造函数。

如下所示,构造函数SubType没有使用默认原型,而是使用SuperType的实例。SubType的实例instance通过内部的[[Prototype]]指向了SubType.prototypeSubType.prototype就是SuperType的实例。

此处需要注意:SubType.prototypeconstructor属性被重写为指向SuperType,因为SubType的原型是SuperType的实例,不存在constructor属性,但是可以通过SuperType的实例访问到SuperType构造函数的原型。

    function SuperType() {
      this.property = true;
    }
    SuperType.prototype.getSuperValue = function () {
      return this.property;
    };
    function SubType() {
      this.subproperty = false;
    }
    // 继承SuperType
    SubType.prototype = new SuperType();

    console.log(SubType.prototype);
    SubType.prototype.getSubValue = function () {
      return this.subproperty;
    };
    let instance = new SubType();
    console.log(instance.getSuperValue()); // true

默认情况下,所有引用类型都是继承自Object,这是通过原型链实现的。

原型与继承关系

原型与继承的关系可以通过两种方式来确定。

instanceof

如果一个实例的原型链中出现过相应的构造函数,则instanceof返回true

console.log(instance instanceof Object); // true
console.log(instance instanceof SubType); // true
isPrototypeOf()

原型链中的每个原型都可以调用这个方法,只要原型链包含这个原型,这个方法就返回true。

console.log(Object.prototype.isPrototypeOf(instance)); // true

原型链实现继承的问题

1.在原型中包含引用值的时候,会在所有实例间共享,因为原型链实现继承它的原型是父构造函数的实例。

2.子类型在实例化时不能给父类型的构造函数传参。

盗用构造函数实现继承

也称对象伪装或者经典继承,其实就是在子类构造函数中调用父类构造函数。关键在于调用父类构造函数时将this指向子类构造函数创建的新对象

如下所示,使用Son创建实例时,每个实例都有自己的family属性。

通过盗用构造函数的一个优点就是可以在子类构造函数中往父类构造函数传递参数。

  function Parent(name) {
    this.family = ["father", "mather"];
    this.name = name;
  }
  Parent.prototype.say = function () {
    console.log("hello");
  };
  function Son() {
    // 继承Parent
    Parent.call(this, "儿子");
  }
  let s1 = new Son();
  s1.family.push("son1");
  let s2 = new Son();
  s2.family.push("son2");
  console.log(s1.family, s1.name); // ['father', 'mather', 'son1'] 儿子
  console.log(s2.family, s2.name); // ['father', 'mather', 'son2'] 儿子
  console.log(s1) // 打印后发现s1的[[Prototype]]的constructor是指向Son的
  s1.say() // 报错

盗用构造函数的问题

必须在构造函数中定义方法,函数无法重用。

子类不能访问父类原型上定义的方法。

组合继承

综合了原型链和构造函数,将两者的优点集中起来,也称伪经典继承。思路就是使用原型链继承原型上的属性和方法,通过盗用构造函数继承实例属性。可以把方法定义在原型上实现重用,又可以让每个实例都有自己的属性。

  function Parent(name) {
    this.family = ["father", "mather"];
    this.name = name;
  }
  Parent.prototype.say = function () {
    console.log("hello", this.name);
  };
  function Son(name, age) {
    // 继承属性
    Parent.call(this, name);
    this.age = age;
  }
  // 继承方法
  Son.prototype = new Parent();
  Son.prototype.saySon = function () {
    console.log("这是儿子", this.name);
  };

  let s1 = new Son("张三", 18);
  let s2 = new Son("李四", 20);
  s1.say(); // hello 张三
  s1.saySon(); // 这是儿子 张三
  s2.say(); // hello 李四
  s2.saySon(); // 这是儿子 李四

原型式继承

原型式继承主要依赖于一个函数,即使不自定义类型也可以通过原型实现对象之间的信息共享。

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

这个函数会创建一个临时构造函数,将传入的对象赋值给这个构造函数的原型,然后返回这个临时类型的实例。适用于有一个对象,然后在这个对象的基础上再创建一个新对象。但是需要注意的是,通过该方式创建的新对象原型上的引用值属性会被共享。

    let person = {
      name: "Nicholas",
      friends: ["Shelby", "Court", "Van"],
    };
    let anotherPerson = object(person);
    anotherPerson.name = "Greg";
    anotherPerson.friends.push("Rob"); // friends属性添加数据,但是friends是引用类型,因此会被共享
    let yetAnotherPerson = object(person);
    yetAnotherPerson.name = "Linda";
    yetAnotherPerson.friends.push("Barbie");
    console.log(person.friends); // ["Shelby","Court","Van","Rob","Barbie"]
    console.log(yetAnotherPerson.friends); // ["Shelby","Court","Van","Rob","Barbie"]
    console.log(anotherPerson.friends); // ["Shelby","Court","Van","Rob","Barbie"]

这种方式和Object.create()有些类似,该方法接受两个参数,作为新对象原型的对象,以及给新对象定义额外属性的对象(第二个可选)。第二个参数需要通过各自的描述符来描述。

let person = {
name: "Nicholas",
friends: ["Shelby", "Court", "Van"]
};
let anotherPerson = Object.create(person, {
    name: {
        value: "Greg"
    }
});
console.log(anotherPerson.name); // "Greg"

寄生式继承

这种继承与上述的原型式继承比较接近,基本思路就是:创建一个实现继承的函数,然后增加对象,返回这个对象 通过该方式创建的对象,具有传入对象的属性和方法,还可以自己定义方法。

  function object(o) {
    function F() {}
    F.prototype = o;
    return new F();
  }
  
  function createObj(obj) {
    let clone = object(obj); // 通过调用函数创建一个新对象
    clone.sayHi = function () {
      // 以某种方式增强这个对象
      console.log("Hi");
    };
    return clone; // 返回这个对象
  }

但是这个方式给对象添加函数会导致函数难以重用。

寄生式组合继承

组合继承中父类构造函数会被调用两次,一次是创建子类原型时,一次是在子类构造函数中调用。寄生式组合通过盗用构造函数继承属性,但使用混合式原型链继承方法。

基本思路是不通过调用父类构造函数给子类原型赋值,而是取得父类原型的一个副本。就是使用寄生式继承来继承父类原型,然后将返回的新对象赋值给子类原型。

    function object(o) {
      function F() {}
      F.prototype = o;
      return new F();
    }
    function inheritPrototype(subType, superType) {
      let prototype = object(superType.prototype); // 创建对象
      prototype.constructor = subType; // 增强对象
      subType.prototype = prototype; // 赋值对象
    }

这个inheritPrototype()函数实现了寄生式组合继承的核心逻辑。这个函数接收两个参数:子类构造函数和父类构造函数。在这个函数内部,第一步是创建父类原型的一个副本。然后,给返回的prototype 对象设置 constructor 属性,解决由于重写原型导致默认 constructor 丢失的问题。最后将新创建的对象赋值给子类型的原型。

    function SuperType1(name) {
      this.name = name;
      this.colors = ["red", "blue", "green"];
    }
    SuperType1.prototype.sayName = function () {
      console.log(this.name);
    };
    function SubType1(name, age) {
      SuperType1.call(this, name);
      this.age = age;
    }
    inheritPrototype(SubType1, SuperType1);
    SubType1.prototype.sayAge = function () {
      console.log(this.age);
    };

寄生式组合继承可以算是引用类型继承的最佳模式。