Skip to content

Class 继承

简介

Class 可以通过 extends 关键字实现继承,让子类继承父类的属性和方法

js
class Point {}

class ColorPoint extends Point {}

super 函数:可以调用父类方法

js
class Point {
  /* ... */
}

class ColorPoint extends Point {
  constructor(x, y, color) {
    super(x, y) // 调用父类的constructor(x, y)
    this.color = color
  }

  toString() {
    return this.color + ' ' + super.toString() // 调用父类的toString()
  }
}

ES6 规定,子类必须在 constructor 方法中调用 super,否则就会报错。

原因:子类 this 对象的构造,先通过对父类构造函数,得到与父类相同的实例属性和方法,然后加工,添加自己的实例属性和方法。不调用 super 子类就得不到自己的 this 对象

ES5 继承机制:先创造一个独立的子类的实例对象,然后再将父类的方法添加到这个对象上面,即“实例在前,继承在后”

ES6 继承机制:先将父类的属性和方法,加到一个空的对象上面,然后再将该对象作为子类的实例,即“继承在前,实例在后”

意味父类构造必须先运行一次

js
class Foo {
  constructor() {
    console.log(1)
  }
}

class Bar extends Foo {
  constructor() {
    super()
    console.log(2)
  }
}

const bar = new Bar()
// 1
// 2

注意:子类构造函数中,调用 super()之后,才可以使用 this 关键字,否则会报错

子类没有定义 constructor 方法,会默认添加,自动调用 super 函数

js
class ColorPoint extends Point {}

// 等同于
class ColorPoint extends Point {
  constructor(...args) {
    super(...args)
  }
}

除了私有属性和方法,父类的所有属性和方法,都会被子类继承,其中包括静态方法

原因:私有属性和方法只能在定义它的 class 里面使用

js
class Foo {
  #p = 1
  #m() {
    console.log('hello')
  }
}

class Bar extends Foo {
  constructor() {
    super()
    console.log(this.#p) // 报错
    this.#m() // 报错
  }
}

注:子类在父类作用域下能取到 私有属性

js
class A {
  #foo = 0
  static test(obj) {
    console.log(#foo in obj)
  }
}

class SubA extends A {}

A.test(new SubA()) // true

父类定义了私有属性的读写方法,子类就可以通过这些方法,读写私有属性

js
class Foo {
  #p = 1
  getP() {
    return this.#p
  }
}

class Bar extends Foo {
  constructor() {
    super()
    console.log(this.getP()) // 1
  }
}

Object.getPrototypeOf

Object.getPrototypeOf 方法:取子类上获取父类

js
class Point {
  /*...*/
}

class ColorPoint extends Point {
  /*...*/
}

Object.getPrototypeOf(ColorPoint) === Point
// true

super 关键字

super 关键字,两个用法

  1. 作函数使用

super 作为函数调用时,代表父类的构造函数。

js
class A {
  constructor() {
    console.log(new.target.name)
  }
}
class B extends A {
  constructor() {
    super()
    // super 等效于
    // A.prototype.constructor.call(this)
  }
}
new A() // A
new B() // B

作为函数使用,只能在构造函数中使用,其余地方使用会报错

  1. 作对象使用

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

js
class A {
  p() {
    return 2
  }
}

class B extends A {
  constructor() {
    super()
    // A.prototype.p()
    console.log(super.p()) // 2
  }
}

let b = new B()

super 指向父类的原型对象,所以无法调用父类实例上的方法或属性

js
class A {
  constructor() {
    this.p = 2
  }
}

A.prototype.x = 2

class B extends A {
  test() {
    console.log(super.p) // undefined
    console.log(super.x) // 2
  }
}

let b = new B()
b.test()

ES6 规定,在子类普通方法中通过 super 调用父类的方法时,方法内部的 this 指向当前的子类实例

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

class B extends A {
  constructor() {
    super()
    this.x = 2
  }
  m() {
    super.print()
  }
}

let b = new B()
b.m() // 2

通过 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.x = 3 ,等同于 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

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

子类的静态方法中通过 super 调用父类的方法时,方法内部的 this 指向当前的子类,而不是子类的实例

js
class A {
  constructor() {
    this.x = 1
  }
  static print() {
    console.log(this.x)
  }
}

class B extends A {
  constructor() {
    super()
    this.x = 2
  }
  static m() {
    super.print()
  }
}

B.x = 3
B.m() // 3

总结:

  1. super 在静态方法之中指向父类,在普通方法之中指向父类的原型对象。即在静态方法中指向静态方法,普通方法指向普通方法。
  2. super 中 this,普通方法指向子类实例,静态方法指向子类

注:使用 super 关键字,必须显式指定是作为函数、还是作为对象使用,否则会报错

js
class A {}

class B extends A {
  constructor() {
    super();
    console.log(super); // 报错
  }
}
js
class A {}

class B extends A {
  constructor() {
    super()
    console.log(super.valueOf() instanceof B) // true
  }
}

let b = new B()

super.valueOf()表明 super 是一个对象,因此就不会报错。由于 super 使得 this 指向 B 的实例,所以 super.valueOf()返回的是一个 B 的实例。

对象总是继承其他对象的,在任意一个对象中,都可以使用 super 关键字

js
var obj = {
  toString() {
    return 'MyObject: ' + super.toString()
  }
}

obj.toString() // MyObject: [object Object]

类的 prototype 属性和__proto__属性

ES5 实现之中,每一个对象都有__proto__属性,指向对应的构造函数的 prototype 属性

Class 作为构造函数的语法糖,同时有 prototype 属性和__proto__属性,因此同时存在两条继承链

(1)子类的__proto__属性:总是指向父类

(2)子类 prototype 属性的__proto__属性,表示方法的继承,总是指向父类的 prototype 属性

js
class A {}

class B extends A {}

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

类的继承实现模式如下

js
class A {}

class B {}

// B 的实例继承 A 的实例
Object.setPrototypeOf(B.prototype, A.prototype)
// B.prototype.__proto__ = A.prototype;

// B 继承 A 的静态属性
Object.setPrototypeOf(B, A)
// B.__proto__ = A;
const b = new B()

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

第一种,子类继承 Object 类

js
class A extends Object {}

A.__proto__ === Object // true
A.prototype.__proto__ === Object.prototype // true

第二种,不存在任何继承

js
class A {}

A.__proto__ === Function.prototype // true
A.prototype.__proto__ === Object.prototype // true

A 作为一个基类,就是一个普通函数,所以直接继承 Function.prototype

实例的 __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

原生构造函数的继承

原生构造函数:指语言内置的构造函数

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

原生构造函数是无法继承的,无法定义一个子类

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

MyArray.prototype = Object.create(Array.prototype, {
  constructor: {
    value: MyArray,
    writable: true,
    configurable: true,
    enumerable: true
  }
})
js
var colors = new MyArray()
colors[0] = 'red'
colors.length // 0

colors.length = 0
colors[0] // "red"

问题:子类无法获得原生构造函数的内部属性,通过Array.apply()或者分配给原型对象都不行。

原因:原生构造函数会忽略apply方法传入的this,也就是说,原生构造函数的this无法绑定,导致拿不到内部属性

ES5 是先新建子类的实例对象 this,再将父类的属性添加到子类上,由于父类的内部属性无法获取,导致无法继承原生的构造函数。

比如,Array构造函数有一个内部属性[[DefineOwnProperty]],用来定义新属性时,更新length属性,这个内部属性无法在子类获取,导致子类的length属性行为不正常。

例子:让一个普通对象继承Error对象

js
var e = {}

Object.getOwnPropertyNames(Error.call(e))
// [ 'stack' ]

Object.getOwnPropertyNames(e)
// []

想通过 Error.call(e)这种写法,让普通对象 e 具有 Error 对象的实例属性。但 Error.call()完全忽略传入的第一个参数,而是返回一个新对象,e 本身没有任何变化。再次说明,通过 call 方法无法继承原生对象。

ES6 允许继承原生构造函数定义子类,即 ES6 可以自定义原生数据结构

原理:ES6 是先新建父类的实例对象 this,然后再用子类的构造函数修饰 this,使得父类的所有行为都可以继承

js
class MyArray extends Array {
  constructor(...args) {
    super(...args)
  }
}

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

arr.length = 0
arr[0] // undefined

例子:自定义 Error 子类

js
class ExtendableError extends Error {
  constructor(message) {
    super()
    this.message = message
    this.stack = new Error().stack
    this.name = this.constructor.name
  }
}

class MyError extends ExtendableError {
  constructor(m) {
    super(m)
  }
}

var myerror = new MyError('ll')
myerror.message // "ll"
myerror instanceof Error // true
myerror.name // "MyError"
myerror.stack
// Error
//     at MyError.ExtendableError
//     ...

注:ES6 改变了 Object 构造函数的行为,一旦发现 Object 方法不是通过 new Object()这种形式调用,ES6 规定 Object 构造函数会忽略参数

js
class NewObj extends Object {
  constructor() {
    super(...arguments)
  }
}
var o = new NewObj({ attr: true })
o.attr === true // false

Mixin 模式的实现

Mixin 指的是多个对象合成一个新的对象,新对象具有各个组成成员的接口。

js
const a = {
  a: 'a'
}
const b = {
  b: 'b'
}
const c = { ...a, ...b } // {a: 'a', b: 'b'}

多个类的接口“混入”(mix in)另一个类

js
function mix(...mixins) {
  class Mix {
    constructor() {
      for (let mixin of mixins) {
        copyProperties(this, new mixin()) // 拷贝实例属性
      }
    }
  }

  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 DistributedEdit extends mix(Loggable, Serializable) {
  // ...
}

相关链接

[-] Class 继承