原型与继承

104 阅读7分钟

继承转转于JavaScript常用八种继承方案

原型链

  • 什么是原型链:由于_proto_是任何对象都有的属性,而js里万物皆对象,所以会形成一条_proto_连起来的链条,递归访问_proto_必须最终到头,并且值是null。
  • 当js引擎查找对象的属性时,先查找对象本身是否存在该属性,如果不存在,会在原型链上查找,但不会查找自身的prototype;

原型链查找机制:

  • 访问对象实例属性,有则返回,没有就通过_proto_去它的原型对象查找;
  • 原型对象找到即返回,找不到,继续通过原型对象的_proto_查找;
  • 一层一层一直找到Object.prototype,如果找到目标属性即返回,找不到就返回undefined, 不会再往下找,因为再往下Object.prototype.proto = null;

四个概念:

  • js分为函数对象和普通对象,每个对象都有_proto_属性,但是只有函数对象才有prototype属性;
  • Object、Function都是js内置的函数,类似的还有我们常用到的Array、RegExp、Date、Boolean、Number、String;
  • 属性_proto_是一个对象,它有两个属性,constructor和_proto_;
  • 原型对象prototype有一个默认的constructor属性,用于记录实例是由哪个构造函数创建;

两个准则:

    // 有以下构造函数Person,他的原型上有所属种类属性 category = 'people'

    function Person(name, age) {
      this.name = name;
      this.age = age;
    }
    Person.prototype.category = 'people'

    // 通过new Person()创建的person1实例
    let person1 = new Person('小明', 18);

原型链我们遵循以下两个准则:

    // 原型对象(Person.prototype)的constructor指向构造函数本身
    Person.prototype.constructor === Person

    // 实例(person1)的_proto_和原型对象指向同一个地方
    person1._proto_ === Person.prototype

所有构造函数都是Function的实例,所有原型对象都是Object的实例除了Object.prototype。

原型链例子:

    function F(){}
    var f = new F();
    // 构造器
    F.prototype.constructor === F; // true
    F.__proto__ === Function.prototype; // true
    Function.prototype.__proto__ === Object.prototype; // true
    Object.prototype.__proto__ === null; // true

    // 实例
    f.__proto__ === F.prototype; // true
    F.prototype.__proto__ === Object.prototype; // true
    Object.prototype.__proto__ === null; // true

ES5的继承和ES6的继承区别:

  • ES5的继承实质上是先创建子类的实例对象,然后再将父类的方法添加到this上(Parent.call(this));
  • ES6的继承有所不同,实质上是先创建父类的实例对象this,然后再用子类的构造函数修改this。因为子类没有自己的this对象,所以必须先调用父类的super()方法,否则新建实例报错

原型链继承

构造函数、原型和实例之间的关系: 每个构造函数都有一个原型对象,原型对象包含一个指向构造函数的指针,而实例都包含一个原型对象的指针;

继承的本质就是复制,即重写原型对象,代之以一个新类型实例

    function SuperType() {
      this.colors = ["red", "blue", "green"];
    }
    function subType() {}
    // 创建SuperType的实例,并将该实例复制给SubType.prototype
    subType.prototype = new SuperType();

    let instance = new subType();
    instance.colors.push('屎黄');
    console.log(instance.colors); // "red", "blue", "green", "屎黄"

    // 原型链存在缺点:多个实例对引用类型的操作会被篡改
    let instance2 = new subType();
    console.log(instance2.colors); // "red", "blue", "green", "屎黄"

借用构造函数继承

使用父类的构造函数来增强子类,等同于复制父类的实例给子类(不使用原型);

    function SuperType() {
      this.colors = ["red", "blue", "green"];
    }
    function subType() {
      // 继承自SuperType
      SuperType.call(this);
    }
    subType.prototype = new SuperType();

    let instance = new subType();
    instance.colors.push('屎黄');
    console.log(instance.colors); // "red", "blue", "green", "屎黄"

    let instance2 = new subType();
    console.log(instance2.colors); // "red", "blue", "green"

核心代码是SuperType.call(this), 创建子类实例时调用SuperType构造函数,于是SubType的每个实例都会将SuperType中的属性复制一份
缺点:

  • 只能继承父类的实例属性和方法,不能继续原型的属性或方法
  • 无法实现复用,每个子类都有父类实例函数的副本,影响性能

组合继承

组合原型链继承和借用构造函数继承就是组合继承。用原型链实现对原型属性和方法的继承,用借用构造函数技术实现实例属性的继承

    function SuperType(name) {
      this.name = name;
      this.colors = ["red", "blue", "green"];
    }
    SuperType.prototype.sayName = function() {
      console.log(this.name);
    }
    function SubType(name, age) {
      // 继承属性
      // 第二次调用SuperType()
      SuperType.call(this, name);
      this.age = age;

    }
    // 继承方法
    // 构建原型链
    // 第一次调用SuperType()
    SubType.prototype = new SuperType();
    // 重写SubType.prototype的constructor属性,指向自己的构造函数SubType
    SubType.prototype.constructor = SubType;
    SubType.prototype.sayAge = function() {
      console.log(this.age);
    }

    let instance1 = new SubType('张三', 18);
    instance1.colors.push('屎黄');
    console.log(instance1.colors); // "red", "blue", "green", "屎黄"
    instance1.sayName(); // 张三
    instance1.sayAge(); // 18

    let instance2 = new SubType('李四', 36);
    console.log(instance2.colors); // "red", "blue", "green"
    instance2.sayName(); // 李四
    instance2.sayAge(); // 36

缺点:

  • 第一次调用SuperType():给SubType.prototype写入两个属性name,color。
  • 第二次调用SuperType():给instance1写入两个属性name,color。

实例对象instance1上的两个属性就屏蔽了其原型对象SubType.prototype的两个同名属性。所以,组合模式的缺点就是在使用子类创建实例对象时,其原型中会存在两份相同的属性/方法。

原型式继承

利用一个空对象作为中介,将某个对象直接赋值给空对象构造函数的原型

    function object(obj) {
      function F() {};
      F.prototype = obj;
      return new F();
    }
    // object对其传入的对象执行了一次浅复制,将构造函数F的原型直接指向传入的对象。
    let person = {
      name: '张三',
      colors: ['red', 'blue', 'green']
    }

    let instance1 = object(person);
    instance1.name = '李四';
    instance1.colors.push('屎黄');
    console.log(instance1.colors); // "red", "blue", "green", "屎黄"
    console.log(instance1.name);// 李四

    let instance2 = object(person);
    instance2.name = '王五';
    console.log(instance2.colors); // 存在篡改, "red", "blue", "green", "屎黄"
    console.log(instance2.name); // 王五

缺点:

  • 原型链继承多个实例的引用类型属性指向相同,存在篡改的可能
  • 无法传递参数

寄生式继承

在原型式继承的基础上,增强对象,返回构造函数

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

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

    let person = {
      name: '张三',
      colors: ['red', 'blue', 'green']
    }

    let instance1 = SuperObject(person);
    instance1.name = '李四';
    instance1.colors.push('屎黄');
    console.log(instance1.colors); // "red", "blue", "green", "屎黄"
    console.log(instance1.name);// 李四
    instance1.sayHi();// Hi

    let instance2 = SuperObject(person);
    instance2.name = '王五';
    console.log(instance2.colors); // 存在篡改, "red", "blue", "green", "屎黄"
    console.log(instance2.name); // 王五

缺点(同原型式继承):

  • 原型链继承多个实例的引用类型属性指向相同,存在篡改的可能。
  • 无法传递参数

寄生组合式继承

结合借用构造函数传递参数和寄生模式实现继承

  • 子类构造函数的__proto__指向父类构造器,继承父类的静态方法
  • 子类构造函数的prototype的__proto__指向父类构造器的prototype,继承父类的方法。
  • 子类构造器里调用父类构造器,继承父类的属性。
    function inheritPrototype(subType, subperType) {
      let prototype = Object.create(subperType.prototype);// 创建对象,创建父类原型的一个副本;
      prototype.constructor = subType;// 增强对象,弥补因重写原型而失去默认的constructor属性
      subType.prototype = prototype;// 指定对象,将新创建的对象赋值给子类原型
    }

    // 父类初始化实例属性和原型属性
    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);
    }

    let instance1 = new SubType('李四', 36);
    instance1.colors.push('屎黄');
    console.log(instance1.colors); // "red", "blue", "green", "屎黄"
    instance1.sayName();// 李四
    instance1.sayAge();// 36

    let instance2 = new SubType('王五', 18);
    console.log(instance2.colors); // "red", "blue", "green"
    instance2.sayName();// 王五
    instance2.sayAge();// 18

混入方式继承多个对象

    function SuperType(name) {
      this.name = name;
      this.colors = ['red', 'blue', 'green'];
    }
    SuperType.prototype.sayName = function() {
      console.log(this.name);
    }

    function SubType(age) {
      this.age = age;
    }

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

    function MyType(name, age) {
      SuperType.call(this, name);
      SubType.call(this, age);
    }
    // 继承一个类
    MyType.prototype = Object(SuperType.prototype);
    // 混合其他
    Object.assign(MyType.prototype, SubType.prototype);
    // 重新指定constructor
    MyType.prototype.constructor = MyType;

    MyType.prototype.sayHi = function() {
      console.log('Hi');
    }

    let instance1 = new MyType('李四', 36);
    instance1.colors.push('屎黄');
    console.log(instance1.colors); // "red", "blue", "green", "屎黄"
    instance1.sayName();// 李四
    instance1.sayAge();// 36
    instance1.sayHi();// Hi

    let instance2 = new MyType('王五', 18);
    console.log(instance2.colors); // "red", "blue", "green"
    instance2.sayName();// 王五
    instance2.sayAge();// 18
    instance2.sayHi();// Hi

Object.assign会把SubType原型上的函数拷贝到MyType原型上,使得MyType的所有实例都可以用SubType的方法

ES6类继承extends

extends关键字主要用于类声明或者类表达式中,以创建一个类,该类是另一个类的子类。其中constructor表示构造函数,一个类中只能有一个构造函数,有多个会报出SyntaxError错误,如果没有显示指定构造函数,则会默认添加constructor方法

    class SuperType {
      constructor(name, age) {
        this.name = name;
        this.age = age;
        this.colors = ['red', 'blue', 'green'];
      }

      // Getter
      get export() {
        return this.group();
      }

      group() {
        return `${this.name}${this.age}岁,颜色是${this.colors}`
      }
    }

    let instance = new SuperType('张三', 16);
    console.log(instance.export);// 张三16岁,颜色是red,blue,green

    // 继承
    class SubType extends SuperType {
      constructor(name, age, sex) {
        super(name, age);
        // 如果之类中存在构造函数,则需要在使用'this'之前首先调用super()
        this.sex = sex;
      }
      get subExport() {
        return `${this.name}${this.age}岁,性别${this.sex},颜色是${this.colors}`
      }
    }

    let instance1 = new SubType('李四',36,'男');
    console.log(instance1.subExport);// 李四36岁,性别男,颜色是red,blue,green