Skip to content

class 继承

继承表示“在已有类的基础上继续扩展”。子类可以复用父类的属性和方法,再添加自己的能力。

基本写法

使用 extends 让一个类继承另一个类:

js
class Animal {
  constructor(name) {
    this.name = name
    this.speed = 0
  }

  run(speed) {
    this.speed = speed
    console.log(`${this.name} runs with speed ${this.speed}`)
  }
}

class Rabbit extends Animal {
  hide() {
    console.log(`${this.name} hides`)
  }
}

const rabbit = new Rabbit('White Rabbit')
rabbit.run(5)
rabbit.hide()

这里 Rabbit 自己只定义了 hide(),但依然可以调用从 Animal 继承来的 run()

继承到底继承了什么

最实用的一层理解是:

  • 子类实例可以访问父类原型上的方法。
  • 子类也可以有自己的方法。
  • 子类和父类依然是两个不同的类。
js
class Animal {
  run() {}
}

class Rabbit extends Animal {
  hide() {}
}

这时:

  • rabbit.hide() 来自 Rabbit.prototype
  • rabbit.run() 来自 Animal.prototype

原型链上的关系

extends 本质上还是建立在原型机制之上:

js
class Animal {}
class Rabbit extends Animal {}

console.log(Object.getPrototypeOf(Rabbit.prototype) === Animal.prototype) // true
console.log(new Rabbit() instanceof Animal) // true

也就是说,类继承只是把原型链组织得更清晰。

重写方法

子类可以定义与父类同名的方法,这会覆盖父类实现:

js
class Animal {
  speak() {
    console.log('animal sound')
  }
}

class Dog extends Animal {
  speak() {
    console.log('wang')
  }
}

调用时会优先命中子类自己的实现。

super

在方法里调用父类方法

如果子类不是完全替换,而是想在父类逻辑基础上扩展,可以使用 super

js
class Animal {
  stop() {
    this.speed = 0
    console.log(`${this.name} stops`)
  }
}

class Rabbit extends Animal {
  stop() {
    super.stop()
    this.hide()
  }

  hide() {
    console.log(`${this.name} hides`)
  }
}

这里的 super.stop() 表示先执行父类版本,再追加子类自己的行为。

在 constructor 里调用父类构造器

js
class Animal {
  constructor(name) {
    this.name = name
  }
}

class Rabbit extends Animal {
  constructor(name, earLength) {
    super(name)
    this.earLength = earLength
  }
}

子类 constructor 里的硬性规则

在继承场景下,如果子类自己写了 constructor,就必须先调用 super(),之后才能访问 this

js
class Animal {
  constructor(name) {
    this.name = name
  }
}

class Rabbit extends Animal {
  constructor(name) {
    super(name)
    this.type = 'rabbit'
  }
}

下面这种写法会报错:

js
class Rabbit extends Animal {
  constructor(name) {
    // this.name = name // ReferenceError
    super(name)
  }
}

原因可以直接记成一句话:子类实例的初始化要先交给父类打底。

不写 constructor 时会怎样

如果子类没有显式声明 constructor,JavaScript 会帮它补一个等价版本:

js
class Rabbit extends Animal {
  constructor(...args) {
    super(...args)
  }
}

所以很多简单子类根本不需要手写构造函数。

继承静态成员

继承的不只是实例方法,静态成员也会沿着类本身继续传递:

js
class Animal {
  static category = 'creature'

  static create() {
    return new this()
  }
}

class Rabbit extends Animal {}

console.log(Rabbit.category) // 'creature'
console.log(Rabbit.create() instanceof Rabbit) // true

extends 后面不一定非要是类名

只要最终能得到一个可继承的构造器,也可以放在 extends 后面:

js
function createBase() {
  return class {
    sayHi() {
      console.log('hi')
    }
  }
}

class User extends createBase() {}

这种写法存在,但日常业务里并不常见。大多数情况下,直接继承一个明确类名更直观。

什么时候适合用继承

继承更适合“明确的 is-a 关系”:

  • Rabbit 是一种 Animal
  • AdminUser 是一种 User

如果只是想复用部分能力,而不是表达类型层级,通常更适合:

  • 组合
  • 工具函数
  • mixin

使用建议

  • 只有在确实存在明确层级关系时,再使用继承。
  • 子类重写方法时,先想清楚是完全替换还是在父类逻辑上扩展。
  • 子类写 constructor 时,先调用 super() 再访问 this
  • 复用逻辑如果只是横切能力,不要优先想到继承。

基于 MIT 许可发布