跳转到内容

Class的继承

简介

比起 ES5 修改原型链实现继承的方式,ES6 的 class 通过 extends 关键字实现继承,更加清晰和方便。子类继承父类的所有属性和方法,需要用 super 方法,继承父类的 this 对象,否则会因为得不到 this 对象新建实例报错。

js
class A {
    x = 1
    y = 2
}

class B extends A {
    constructor() {
        super()
    }
}

class C extends A {
    constructor() {
    }
}

let b = new B() // B {x: 1, y: 2}
let c = new C() // Uncaught ReferenceError: Must call super constructor in derived class before accessing 'this' or returning from derived constructor

ES5 继承实质是先创建子类的实例对象 this,然后再将父类的方法添加到 this 上(Parent.apply(this))。ES6 继承实质是先创建父类的实例对象 this(所以必须先调用 super 方法),然后再用子类的构造函数修改 this

如果子类没有定义 constructor 方法,这个方法会被默认添加,代码如下。也就是说,不管有没有显式定义,任何一个子类都有 constructor 方法。且只有调用了 super 方法,子类实例的 this 才会被初始化,才能使用 this

js
class A {}

class B extends A {}

// 等同于
class B extends A {
    constructor(...args) {
        super(...args)
    }
}

class C extends A {
    constructor(x) {
        this.x = x // Error
        super()
        this.x = x // OK
    }
}

Object.getPrototypeOf()

Object.getPrototypeOf 方法可以用来从子类上获取父类。因此可以用这个方法判断某个子类是否继承了另一个类作为父类。

js
Object.getPrototypeOf(B) === A // true

super关键字

super 关键字既可以当作函数使用,也可以当作对象使用。

  • 在作为函数使用时,代表父类的构造函数

    ES6 要求,子类的构造函数必须执行一次 super 函数,否则新建实例会报错。

    super 虽然代表了父类 A 的构造函数,但是返回的是子类 B 的实例,即 super 内部的 this 指的是 B 的实例,因此 super() 在子类 B 的构造函数中执行时,它内部的 this 指向的是 B 的实例。

    另外,super 函数只能用在子类的构造函数之中,用在其他地方会报错。

    js
      class A {
        constructor() {
          console.log(new.target.name)
        }
      }
    
      class B extends A {
        constructor() {
          super()
        }
    
        fn() {
          super() // 报错
        }
      }
    
      new A() // A
      new B() // B
  • 在作为对象使用时,指向父类的原型对象

    super 作为对象时,在普通方法中,指向父类的原型对象;在静态方法中,指向父类。

    ⚠ 注意

    由于 super 指向父类的原型对象,所以定义在父类实例上的方法或属性,是无法通过 super 调用的。把属性定义在父类的原型对象上 super 才能拿到。

    js
      class A {
        constructor() {
          this.foo = 1
        }
        p() {
          return 2
        }
      }
      A.prototype.bar = 3
    
      class B extends A {
        constructor() {
          super()
          console.log(super.p()) // 2
          console.log(super.bar) // 3
        }
    
        get m() {
          return super.foo // undefined
        }
      }
    
      let b = new B()

ES6规定,通过 super 调用父类方法时,super 会绑定子类的 this 。因此,如果通过 super 对某个属性赋值,这时 super 就是 this,赋值的属性会变成子类实例的属性。

js
class A {
  constructor() {
    this.x = 1
  }
}

class B extends A {
  constructor() {
    super()
    this.x = 2
    super.x = 3
    console.log(super.x) // undefined
    console.log(this.x) // 3
  }
}

let b = new B()

上方代码中,用 super 赋值相当于 this.x = 3 ,当读取 super.x 时,读的是 A.prototype.x ,所以结果为 undefined

如果 super 作为对象用在静态方法中,super 会指向父类,而不是父类的原型对象。

查看代码
js
class Parent {
  static myMethod(msg) {
    console.log('static', msg)
  }

  myMethod(msg) {
    console.log('instance', msg)
  }
}

class Child extends Parent {
  static myMethod(msg) {
    super.myMethod(msg)
  }

  myMethod(msg) {
    super.myMethod(msg)
  }
}


Child.myMethod(1) // static 1

let child = new Child()
child.myMethod(2) // instance 2

⚠ 注意

使用 super 的时候,必须显式指定是作为函数、还是作为对象使用,否则会报错。

类的prototype属性和__proto__属性

在大多数浏览器的 ES5 实现之中,每一个对象都有 __proto__ 属性,指向对应的构造函数的 prototype 属性。Class 作为构造函数,也有 prototype 属性和 __proto__ 属性,因此同时存在两条继承链。

  • 子类的 __proto__ 属性,表示构造函数的继承,总是指向父类。
  • 子类 prototype 属性的 __proto__ 属性,表示方法的继承,总是指向父类的 prototype 属性。
js
class A {}

class B extends A {}

B.__proto__ === A // true
B.prototype.__proto__ === A.prototype // true

上面代码中,子类 B__proto__ 属性指向父类 A ,子类 Bprototype 属性的 __proto__ 属性指向父类 Aprototype 属性。

extends的继承目标

extends 关键字后面可以跟多种类型的值。

js
class B extends A {}

上面代码的 A 可以是任意值。只要这个值有 prototype 属性即可。由于函数都有 prototype 属性,因此 A 可以是任意函数。

不过,有三种特殊情况:

  1. 子类继承 Object

    js
    class A extends Object {}
    A.__proto__ === Object // true
    A.prototype.__proto__ === Object.prototype // true

    这种情况下,A 其实就是构造函数 Object 的复制,A 的实例就是 Object 的实例。

  2. 不存在任何继承

    js
    class A {}
    A.__proto__ === Function.prototype // true
    A.prototype.__proto__ === Object.prototype // true

    这种情况下,A 作为一个基类(即不存在任何继承),就是一个普通函数,所以直接继承 Function.prototype 。但是,A 调用 super 方法时,返回的是 undefined ,因为 A 作为一个普通函数,没有任何继承。

  3. 子类继承 null

    js
    class A extends null {}
    A.__proto__ === Function.prototype // true
    A.prototype.__proto__ === undefined // true

    这种情况下,A 也是一个普通函数,所以直接继承 Function.prototype 。但是,A 调用 super 方法时,返回的是 undefined ,因为没有继承任何东西。

实例的 proto 属性

子类实例的 __proto__ 属性的 __proto__ 属性,指向父类实例的 __proto__ 属性。也就是说,子类的原型的原型,是父类的原型。

js
var p1 = new Point(2, 3);
var p2 = new ColorPoint(2, 3, 'red');

p2.__proto__ === p1.__proto__ // false
p2.__proto__.__proto__ === p1.__proto__ // true

原生构造函数的继承

原生构造函数是指语言内置的构造函数,通常用来生成数据结构。ECMAScript 的原生构造函数大致有下面这些。

  • Boolean()
  • Number()
  • String()
  • Array()
  • Date()
  • Function()
  • RegExp()
  • Error()
  • Object()

以前,这些原生构造函数是无法继承的,比如,不能自己定义一个 Array 的子类。

js
function MyArray() {
  Array.apply(this, arguments)
}

MyArray.prototype = Object.create(Array.prototype, {
  constructor: {
    value: MyArray,
    writable: true,
    configurable: true,
    enumerable: true
  }
})

var arr = new MyArray()
arr[0] = 12
arr.length // 0

上面代码中,MyArray 并不是 Array 的实例,所以 arr.length 属性等于0。这显然不符合继承的意图,所以 ES6 允许继承原生构造函数定义子类,因为 ES6 是先新建父类的实例对象 this ,然后再用子类的构造函数修饰 this ,使得父类的所有行为都可以继承。

Mixin模式的实现

由于 ES6 没有 mixin 的语法,但是通过 extends 关键字,可以很方便地实现 mixin 模式。具体做法如下。

首先,实现一些 mixin 函数。

js
function mix(...mixins) {
  class Mix {}

  for (let mixin of mixins) {
    copyProperties(Mix, mixin);
    copyProperties(Mix.prototype, mixin.prototype);
  }

  return Mix;
}

function copyProperties(target, source) {
  for (let key of Reflect.ownKeys(source)) {
    if (key !== 'constructor' && key !== 'prototype' && key !== 'name') {
      let desc = Object.getOwnPropertyDescriptor(source, key);
      Object.defineProperty(target, key, desc);
    }
  }
}

然后,定义一些类。

js
class MyClass extends mix(AnotherClass, YetAnotherClass) {
  /* ... */
}

上面代码中,MyClass 就是 AnotherClassYetAnotherClass 的混合,继承了它们的全部接口。

总结

ES6 的 class 语法通过 extends 关键字实现了继承,使得子类可以继承父类的属性和方法。子类的构造函数中必须首先调用 super() 方法,以确保父类的构造函数被正确执行,从而正确地初始化子类的 this 对象。如果子类没有显式定义构造函数,那么会默认添加一个调用了 super() 的空构造函数。

super 关键字在子类中具有双重作用:作为函数时,它代表父类的构造函数;作为对象时,在普通方法中指向父类的原型对象,而在静态方法中指向父类本身。通过 super,子类可以调用父类的方法,同时 super 会绑定子类的 this,使得对属性的赋值会反映到子类实例上。

ES6 的继承机制同时存在两条继承链:一条是子类的 __proto__ 属性指向父类,表示构造函数的继承;另一条是子类 prototype 属性的 __proto__ 属性指向父类的 prototype 属性,表示方法的继承。这种设计使得子类可以继承父类的实例属性和原型上的方法。

extends 关键字后可以跟任意具有 prototype 属性的函数,包括原生构造函数。ES6 允许继承原生构造函数,解决了 ES5 中无法通过原型链继承原生构造函数的问题。继承原生构造函数时,子类可以正确地继承和扩展原生对象的行为。

除了传统的继承模式,ES6 还可以实现 Mixin 模式,即通过 extends 和一些辅助函数,使得一个类可以继承多个类的属性和方法。这种模式提高了代码的复用性,允许开发者创建更加灵活和功能丰富的类结构。