技术知识文章集合TECHNICAL ARCHIVE · 457 DOCUMENTS

显示模式

登录
ARCHIVE DOCUMENTJS

JavaScript 实现继承的方式

所属馆藏
JavaScript
文件格式
Markdown
原始路径
JavaScript/23-JavaScript 实现继承的方式
本文目录10 个章节
  1. 一、原型链是什么
  2. 二、类式继承:Child.prototype = new Parent()
  3. 三、构造函数继承:调用父构造函数
  4. 四、组合继承:实例属性 + 原型方法
  5. 五、寄生组合式继承:使用 Object.create
  6. 六、ES2015 class 与 extends
  7. 七、继承关系与 instanceof
  8. 八、现代项目是否应该优先继承
  9. 九、常见错误总结
  10. 参考资料与原文

JavaScript 实现继承的方式

Category(分类): JavaScript Status: 已整理(2026)

本文保留原文介绍“类式继承、构造函数继承、组合继承、寄生组合式继承和 extends 继承”的结构,同时把原文中被复制按钮挤在同一行的代码重新排版。JavaScript 本质上是原型对象之间的委托;class 是建立在原型机制之上的语法和语义抽象,不是另一种完全不同的对象模型。

一、原型链是什么

当读取对象自身没有的属性时,JavaScript 会继续沿着该对象的内部 [[Prototype]] 链查找,直到找到属性或到达 null。方法通常放在原型上,让同一类实例共享方法函数:

const animal = {
  greet() {
    return '汪汪或喵喵'
  }
}

const dog = Object.create(animal)
dog.name = '旺财'

console.log(dog.name) // 旺财:对象自身属性
console.log(dog.greet()) // 汪汪或喵喵:从原型链找到
console.log(Object.getPrototypeOf(dog) === animal) // true

Object.getPrototypeOf() 是读取原型的标准 API。__proto__ 是历史访问器,不建议作为新代码的主要 API;设置原型应优先使用对象字面量的 __proto__ 初始化语法、Object.create()Object.setPrototypeOf(),并注意运行时改变原型可能影响性能。

二、类式继承:Child.prototype = new Parent()

早期 JavaScript 教材常把构造函数称为“类”,并直接把父类实例赋给子类原型:

function Animal(color) {
  this.name = 'animal'
  this.type = ['pig', 'cat']
  this.color = color
}

Animal.prototype.greet = function (sound) {
  return sound
}

function Dog() {
  this.name = 'dog'
}

// 旧式类式继承:这里会立即调用一次 Animal
Dog.prototype = new Animal('白色')
Dog.prototype.constructor = Dog

const dog = new Dog()
console.log(dog.greet('汪汪')) // 汪汪
console.log(dog.color) // 白色

这种写法有两个典型问题:

  1. Dog.prototype 本身是一个 Animal 实例,Animal 构造函数在定义继承关系时就被调用了一次;
  2. 如果父构造函数把数组或对象放在实例上,这些值会成为所有没有自身同名属性的子类实例共享的原型属性。
dog.type.push('dog')
const dog2 = new Dog()
console.log(dog2.type) // ['pig', 'cat', 'dog']:共享了原型上的数组

此外,父构造函数中的 color 只能在设置原型时确定,不能根据每个 Dog 实例分别传入。把 constructor 修复为 Dog 只修复了元信息,不会消除这些共享和初始化问题。因此这种写法主要用于理解历史代码,不建议作为新代码的继承方案。

三、构造函数继承:调用父构造函数

构造函数继承在子构造函数中调用父构造函数,并把 this 指向新建的子实例。这样父构造函数创建的数组和对象会在每个实例上单独创建:

function Animal(color) {
  this.name = 'animal'
  this.type = ['pig', 'cat']
  this.color = color
}

Animal.prototype.greet = function (sound) {
  return sound
}

function Dog(color) {
  Animal.call(this, color)
  this.name = 'dog'
}

const dog = new Dog('白色')
const dog2 = new Dog('黑色')
dog.type.push('dog')

console.log(dog.color) // 白色
console.log(dog.type) // ['pig', 'cat', 'dog']
console.log(dog2.color) // 黑色
console.log(dog2.type) // ['pig', 'cat']
console.log(typeof dog.greet) // undefined:没有继承 Animal.prototype

call/apply 只是调用函数时指定 this 的机制,并不是“复制父类的方法”。因此该方案解决了实例属性共享,却没有建立原型链,也无法直接使用父原型上的方法。

四、组合继承:实例属性 + 原型方法

组合继承把上面两种方式结合起来:在子构造函数中调用父构造函数负责实例属性,再把父实例放进子原型以获得方法:

function Animal(color) {
  this.name = 'animal'
  this.type = ['pig', 'cat']
  this.color = color
}

Animal.prototype.greet = function (sound) {
  return sound
}

function Dog(color) {
  // 第一次:每个 Dog 实例各自初始化实例属性
  Animal.call(this, color)
  this.name = 'dog'
}

// 第二次:设置原型时又调用一次 Animal
Dog.prototype = new Animal()
Dog.prototype.constructor = Dog

const dog = new Dog('白色')
const dog2 = new Dog('黑色')
dog.type.push('dog')

console.log(dog.greet('汪汪')) // 汪汪
console.log(dog.color, dog2.color) // 白色 黑色
console.log(dog.type, dog2.type) // ['pig', 'cat', 'dog'] ['pig', 'cat']

由于实例属性会遮蔽原型上的同名属性,所以数组不再被不同实例共享。但父构造函数被调用两次,设置原型时那次调用创建的 typecolor 通常不会被直接使用,属于多余开销。

五、寄生组合式继承:使用 Object.create

寄生组合式继承保留“调用父构造函数初始化实例属性”,但使用 Object.create(Animal.prototype) 建立原型链,而不是创建一个父实例:

function Animal(color) {
  this.name = 'animal'
  this.type = ['pig', 'cat']
  this.color = color
}

Animal.prototype.greet = function (sound) {
  return sound
}

function Dog(color) {
  Animal.call(this, color)
  this.name = 'dog'
}

Dog.prototype = Object.create(Animal.prototype, {
  constructor: {
    value: Dog,
    writable: true,
    configurable: true,
    enumerable: false
  }
})

Dog.prototype.getName = function () {
  return this.name
}

const dog = new Dog('白色')
const dog2 = new Dog('黑色')
dog.type.push('dog')

console.log(dog.greet('汪汪')) // 汪汪
console.log(dog.getName()) // dog
console.log(dog.type, dog2.type) // ['pig', 'cat', 'dog'] ['pig', 'cat']
console.log(Object.getPrototypeOf(Dog.prototype) === Animal.prototype) // true

Object.create(proto) 创建一个新对象,并把它的原型设置为 proto;它不是把原型对象“浅拷贝”成一个普通对象。constructor 属性需要显式恢复,因为新建的子原型默认没有一个指向 Dog 的自有 constructor 属性。

等价的教学实现如下:

function inheritPrototype(child, parent) {
  child.prototype = Object.create(parent.prototype)
  child.prototype.constructor = child
}

这种写法在 class 出现之前很常见。现在维护旧构造函数代码时仍可能遇到它,但新代码通常优先使用 class 或组合而不是手工拼接继承链。

六、ES2015 classextends

class 提供了更清晰的构造函数和方法语法。类方法仍然位于原型上,实例字段则通常在每个实例上创建:

class Animal {
  constructor(color) {
    this.color = color
    this.type = ['pig', 'cat']
  }

  greet(sound) {
    return sound
  }
}

class Dog extends Animal {
  constructor(color, name) {
    super(color)
    this.name = name
  }

  bark() {
    return this.greet('汪汪')
  }
}

const dog = new Dog('黑色', '旺财')
console.log(dog.bark()) // 汪汪
console.log(dog.color, dog.name) // 黑色 旺财
console.log(Object.getPrototypeOf(Dog.prototype) === Animal.prototype) // true

在派生类构造函数中,通常必须先调用 super(),才能访问 thissuper(color) 会调用父类构造函数;super.greet() 则会从父类原型上查找方法。若派生类没有写构造函数,JavaScript 会提供一个转发参数的默认构造函数。

extends 不只可以继承普通类,也可以继承构造函数或具有构造能力的内置类。class 声明不会被提升到可直接使用的状态,类方法默认不可枚举,类体默认处于严格模式。

私有字段与实例字段

现代类可以使用私有字段。私有字段不是普通字符串属性,不能通过 obj['#value'] 访问:

class Counter {
  #value = 0

  increment() {
    this.#value += 1
    return this.#value
  }

  get value() {
    return this.#value
  }
}

const counter = new Counter()
console.log(counter.increment()) // 1
console.log(counter.value) // 1

如果目标环境不支持私有字段,需要通过构建工具或闭包实现等价封装;不要把 #field 当作可以被任意代码读取的普通属性。

七、继承关系与 instanceof

instanceof 检查右侧构造函数的 prototype 是否出现在左侧对象的原型链中:

class Parent {}
class Child extends Parent {}

const child = new Child()
console.log(child instanceof Child) // true
console.log(child instanceof Parent) // true
console.log(child instanceof Object) // true

它不适合判断跨 Realm 的数组或对象,也可能被自定义的 Symbol.hasInstance 改变。数组应使用 Array.isArray(value);跨窗口对象的类型判断要结合具体 API 和协议。

八、现代项目是否应该优先继承

继承适合表达稳定的“是一个”关系,例如 Dog 是一种 Animal,并且子类确实需要替换或扩展父类行为。以下情况更适合组合:

  • 组件需要复用能力,但不需要共享同一条原型链;
  • 功能可以独立开关,例如日志、缓存、重试;
  • 业务模型变化频繁,继承层级会持续增长;
  • 需要把依赖注入到对象,而不是让子类隐式依赖父类。
function withLogger(service, logger = console) {
  return {
    ...service,
    request(...args) {
      logger.log('request', args)
      return service.request(...args)
    }
  }
}

const service = {
  request(url) {
    return `GET ${url}`
  }
}

const loggedService = withLogger(service)
console.log(loggedService.request('/users'))

组合不是“永远优于继承”,但它可以减少脆弱的基类问题。选择时应以职责、变化方向、测试成本和运行时约束为依据。

九、常见错误总结

  1. Dog.prototype = new Animal() 当成现代 JavaScript 的唯一继承方式;它会共享原型上的引用属性并多调用一次父构造函数。
  2. 以为 Animal.call(this) 会继承 Animal.prototype 的方法;它只是在当前实例上执行父构造函数。
  3. 使用 Object.create() 后忘记修复 constructor;这会让调试和基于构造函数的元信息产生误导。
  4. 派生类构造函数在 super() 之前访问 this;这会抛出 ReferenceError
  5. __proto__ 作为通用类型判断或频繁修改原型;优先使用标准原型 API。
  6. 认为 instanceof 在所有窗口、Worker 和 iframe 中都可靠;跨 Realm 时构造函数身份可能不同。

参考资料与原文

457 DOCUMENTS · 10 COLLECTIONS
ARCHIVE SEARCH457 篇文章

SEARCH GUIDE

输入关键词开始搜索

支持搜索文章标题、所属分类和原始文档路径。

按分类浏览

10 COLLECTIONS