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

显示模式

登录
ARCHIVE DOCUMENTJS

JS 实现继承的几种方式

所属馆藏
JavaScript
文件格式
Markdown
原始路径
JavaScript/46-js实现继承的几种方式
本文目录13 个章节
  1. 一、什么是 JavaScript 中的继承
  2. 二、先理解构造函数和原型
  3. 三、方式 1:对象冒充(借用构造函数)
  4. 四、方式 2:原型链继承(prototype chaining)
  5. 五、方式 3:混合/组合继承
  6. 六、方式 4:寄生组合继承
  7. 七、方式 5:ES6 class extends
  8. 八、ES6 class 的历史 ES5 转译
  9. 九、非构造函数对象之间的继承
  10. 十、历史图示与来源
  11. 十一、如何选择
  12. 十二、总结
  13. 参考资料

JS 实现继承的几种方式

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

原文以几何形状为引子,介绍对象冒充、原型链、混合继承、ES6 extends 和对象之间的继承。本文保留这些历史方案,修复 function/new 粘连、构造函数指针、object(Chinese) 拼写和错误的 ES5 转译代码,并补充 Object.create、静态继承、组合优先和现代 class 语义。

原文:js 实现继承的几种方式

一、什么是 JavaScript 中的继承

继承通常表示:一个对象或类型可以复用另一个对象/类型的行为,并在此基础上增加或覆盖能力。几何形状可以形成直观的关系:圆是某种椭圆,矩形是多边形,正方形又可以是矩形的特化。

但 JavaScript 的继承不是“复制父类全部内容”:

  • 对象通过 [[Prototype]] 委托属性查找;
  • 构造函数负责初始化实例自有状态;
  • 原型对象通常保存可共享的方法;
  • class 是建立这些关系的一种语法和语义,不会消除底层原型机制。

原文中的“所有数据都是对象”“对象是通过克隆原型得到的”属于原型编程的历史观点,不是 ECMAScript 的完整规则。JavaScript 同时拥有 undefinednull、boolean、string、symbol、number、bigint 等原始值。

二、先理解构造函数和原型

function Person(name) {
  this.name = name
}

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

const person = new Person('Ada')

console.log(person.name) // Ada
console.log(person.getName()) // Ada
console.log(Object.getPrototypeOf(person) === Person.prototype) // true
console.log(Person.prototype.constructor === Person) // true

new Person('Ada') 的简化过程是:创建一个新对象,把它的 [[Prototype]] 设置为 Person.prototype,以新对象为 this 执行 Person,再根据构造器返回值决定最终结果。

函数能否作为构造器不是由首字母大小写决定的。普通函数通常可调用且可构造;箭头函数、对象方法、async 函数和 generator 函数通常不可构造:

function Ordinary() {}
const arrow = () => {}
const method = { run() {} }.run

console.log(typeof Ordinary) // function
console.log(typeof arrow) // function
console.log(Reflect.construct(Ordinary, []) instanceof Ordinary) // true

for (const value of [arrow, method]) {
  try {
    Reflect.construct(value, [])
  } catch (error) {
    console.log(error.name) // TypeError
  }
}

三、方式 1:对象冒充(借用构造函数)

因为构造函数本质上也是函数,所以可以用 call/apply 在子类构造器中执行父类构造器:

function ClassA(color) {
  this.color = color
  this.sayColor = function () {
    return this.color
  }
}

function ClassB(color, name) {
  ClassA.call(this, color)
  this.name = name
  this.sayName = function () {
    return this.name
  }
}

const objectA = new ClassA('blue')
const objectB = new ClassB('red', 'John')

console.log(objectA.sayColor()) // blue
console.log(objectB.sayColor()) // red
console.log(objectB.sayName()) // John
console.log(objectB instanceof ClassA) // false:还没有建立原型链

原文还展示了把 ClassA 临时挂到 this.newMethod 再删除的写法:

function LegacyClassB(color, name) {
  this.newMethod = ClassA
  this.newMethod(color)
  delete this.newMethod
  this.name = name
}

这种对象冒充写法只是历史代码。直接 ClassA.call(this, color) 更清晰,也避免临时属性被覆盖、构造过程中抛错后没有删除等问题。

优点:可以传参,父构造器创建的自有属性不会直接由不同实例共享。

缺点:不会自动继承 ClassA.prototype,并且如果父构造器内定义方法,每次创建实例都重复创建函数。现代写法通常把状态初始化和共享方法分开。

四、方式 2:原型链继承(prototype chaining)

function ClassA() {}

ClassA.prototype.color = 'blue'
ClassA.prototype.sayColor = function () {
  return this.color
}

function ClassB() {}

ClassB.prototype = new ClassA()
ClassB.prototype.constructor = ClassB
ClassB.prototype.name = ''
ClassB.prototype.sayName = function () {
  return this.name
}

const objectA = new ClassA()
const objectB = new ClassB()
objectA.color = 'blue'
objectB.color = 'red'
objectB.name = 'John'

console.log(objectA.sayColor()) // blue
console.log(objectB.sayColor()) // red
console.log(objectB.sayName()) // John
console.log(objectB instanceof ClassA) // true
console.log(objectB instanceof ClassB) // true

这里 ClassB.prototype 被设置为 ClassA 的实例,所以实例原型链大致是:

objectB → ClassB.prototype → ClassA.prototype → Object.prototype → null

缺点:

  • 设置原型时调用了 new ClassA(),不能为每个子类实例传递父类参数;
  • 父类构造器创建的引用类型属性可能被放在 ClassB.prototype 上并被所有实例共享;
  • 重新赋值后必须恢复 constructor
  • 父类构造器可能产生不必要的初始化副作用。

现代代码通常用 Object.create(ClassA.prototype) 建立原型链,避免把父类实例作为原型。

五、方式 3:混合/组合继承

组合继承结合借用构造函数和原型链:

function ClassA(color) {
  this.color = color
}

ClassA.prototype.sayColor = function () {
  return this.color
}

function ClassB(color, name) {
  ClassA.call(this, color) // 复制父类自有状态
  this.name = name
}

ClassB.prototype = new ClassA() // 建立原型链,但会第二次调用父构造器
ClassB.prototype.constructor = ClassB
ClassB.prototype.sayName = function () {
  return this.name
}

const objectB = new ClassB('red', 'John')
console.log(objectB.sayColor()) // red
console.log(objectB.sayName()) // John
console.log(objectB instanceof ClassA) // true
console.log(objectB instanceof ClassB) // true

它比单独使用前两种方式更完整,但 ClassA 会调用两次:设置 ClassB.prototype 时一次,构造实例时一次。父类自有属性可能先出现在子类原型,再被实例属性覆盖,形成冗余。

六、方式 4:寄生组合继承

寄生组合继承用 Object.create 只连接父类原型,不调用父类构造器来创建子类原型:

function inherit(Sub, Super) {
  Sub.prototype = Object.create(Super.prototype)
  Object.defineProperty(Sub.prototype, 'constructor', {
    value: Sub,
    enumerable: false,
    writable: true,
    configurable: true
  })

  // 完整模拟 class extends 时建立静态继承;不需要静态继承时可以省略
  Object.setPrototypeOf(Sub, Super)
}

function Person(name) {
  this.name = name
}

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

function Programmer(name, language) {
  Person.call(this, name)
  this.language = language
}

inherit(Programmer, Person)
Programmer.prototype.code = function () {
  return `${this.name} writes ${this.language}`
}

const programmer = new Programmer('Ada', 'JavaScript')
console.log(programmer.getName()) // Ada
console.log(programmer.code()) // Ada writes JavaScript
console.log(programmer instanceof Person) // true
console.log(Object.getPrototypeOf(Programmer) === Person) // true

它解决了传统组合继承重复调用父构造器的问题,是 ES5 时代常见的完整方案。缺点是需要手动处理原型、构造器属性、静态继承和父类初始化顺序,因此新代码一般使用 class extends 或组合。

七、方式 5:ES6 class extends

class Point {
  constructor(x, y) {
    this.x = x
    this.y = y
  }

  getPosition() {
    return [this.x, this.y]
  }
}

class ColorPoint extends Point {
  constructor(x, y, color) {
    super(x, y)
    this.color = color
  }

  getColor() {
    return this.color
  }
}

const point = new ColorPoint(1, 2, 'red')
console.log(point.getPosition()) // [1, 2]
console.log(point.getColor()) // red
console.log(point instanceof Point) // true
console.log(point instanceof ColorPoint) // true
console.log(Object.getPrototypeOf(ColorPoint.prototype) === Point.prototype) // true
console.log(Object.getPrototypeOf(ColorPoint) === Point) // true

class 的重要语义:

  • class 构造器不能脱离 new 直接调用;
  • 派生 class 构造器必须先调用 super() 才能使用 this
  • 方法默认定义在原型上并且不可枚举;
  • extends 同时建立实例原型链和构造器的静态原型链;
  • class 默认使用严格模式;
  • 私有字段、静态块、supernew.target 等能力有自己的规范语义。

因此,class 不是简单的文本替换;它使用原型机制,但还提供了更完整的构造和继承语义。

八、ES6 class 的历史 ES5 转译

旧项目常使用 Babel 等工具把 class 转换为 ES5。转译器版本、目标环境和插件不同,生成代码也会不同,不能把某一份 helper 当作规范实现。下面是便于理解的简化版本:

function inherits(Sub, Super) {
  Sub.prototype = Object.create(Super.prototype, {
    constructor: {
      value: Sub,
      enumerable: false,
      writable: true,
      configurable: true
    }
  })
  Object.setPrototypeOf(Sub, Super)
}

function Point(x, y) {
  this.x = x
  this.y = y
}

Point.prototype.getPosition = function () {
  return [this.x, this.y]
}

function ColorPoint(x, y, color) {
  Point.call(this, x, y)
  this.color = color
}

inherits(ColorPoint, Point)
ColorPoint.prototype.getColor = function () {
  return this.color
}

const point = new ColorPoint(1, 2, 'red')
console.log(point.getPosition()) // [1, 2]
console.log(point.getColor()) // red

这段代码只能模拟公开字段和原型方法;class 私有字段、派生构造器返回值、内建 subclassing、super 等语义并不能仅靠几行 call 完整复现。

九、非构造函数对象之间的继承

原文用“中国人”和“医生”说明对象继承:

const Chinese = {
  nation: '中国'
}

const Doctor = Object.create(Chinese)
Doctor.career = '医生'

console.log(Doctor.nation) // 中国
console.log(Doctor.career) // 医生
console.log(Object.getPrototypeOf(Doctor) === Chinese) // true

原文的空构造函数写法中 var Doctor = object(Chinese) 是拼写错误,应该调用定义的函数:

function inheritObject(prototype) {
  function Temporary() {}
  Temporary.prototype = prototype
  return new Temporary()
}

const Chinese = { nation: '中国' }
const Doctor = inheritObject(Chinese)
Doctor.career = '医生'

console.log(Doctor.nation) // 中国
console.log(Doctor.career) // 医生

现代代码直接使用 Object.create

const Chinese = { nation: '中国' }
const Doctor = Object.create(Chinese, {
  career: {
    value: '医生',
    enumerable: true,
    writable: true,
    configurable: true
  }
})

console.log(Doctor.nation) // 中国
console.log(Doctor.career) // 医生

如果目标只是复用能力,组合通常比继承更灵活:

function createCanCode(name) {
  return {
    name,
    code() {
      return `${this.name} is coding`
    }
  }
}

const developer = createCanCode('Ada')
console.log(developer.code()) // Ada is coding

十、历史图示与来源

原文的几何形状继承图已下载到本地:

几何形状继承历史图示

图片来源:原始图片。图示用于理解“is-a”关系;实际 JavaScript 代码还需要明确 [[Prototype]]、构造器和实例状态的边界。

十一、如何选择

需求推荐方式
旧 ES5 代码维护了解借用构造函数、组合继承和寄生组合继承
新的类型层次class extends,同时理解其原型语义
一个对象委托给另一个对象Object.create 或对象字面量原型语法
复用几个独立能力组合、工厂函数或 mixin,并控制名称冲突
只想共享无状态方法原型方法、class 方法或模块函数
需要跨窗口/Worker 传输使用消息协议和可结构化克隆的数据,不依赖 instanceof

继承层次过深会增加耦合;如果子类只是为了复用一两个方法,直接组合或导入函数往往更清晰。父类构造器应只负责初始化当前实例状态,原型方法应避免持有本应隔离的可变共享数据。

十二、总结

  • 借用构造函数解决状态初始化,但不会自动继承原型方法;
  • 原型链继承可以共享方法,但父类实例状态可能共享或产生初始化副作用;
  • 组合继承兼顾两者,但会调用父构造器两次;
  • 寄生组合继承通过 Object.create 避免设置原型时调用父构造器;
  • class extends 建立实例和静态两条原型关系,并增加 super、私有字段等语义;
  • 对象之间的委托优先使用 Object.create,不要把它误称为深复制;
  • 不要把 Babel 的 ES5 输出当作唯一的 class 实现;
  • 继承不是唯一的复用手段,组合和模块函数经常更适合业务代码。

参考资料

457 DOCUMENTS · 10 COLLECTIONS
ARCHIVE SEARCH457 篇文章

SEARCH GUIDE

输入关键词开始搜索

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

按分类浏览

10 COLLECTIONS