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

显示模式

登录
ARCHIVE DOCUMENTJS

JS 总结之原型继承的几种方式

所属馆藏
JavaScript
文件格式
Markdown
原始路径
JavaScript/41-JS 总结之原型继承的几种方式
本文目录12 个章节
  1. 一、先准备父类
  2. 二、方式 1:原型链继承
  3. 三、方式 2:借用构造函数(经典继承)
  4. 四、方式 3:组合继承
  5. 五、方式 4:原型式继承
  6. 六、方式 5:寄生式继承
  7. 七、方式 6:寄生组合继承
  8. 八、方式 7:ES6 class extends
  9. 九、非构造函数对象的继承
  10. 十、继承不等于必须使用继承
  11. 十一、总结
  12. 参考资料

JS 总结之原型继承的几种方式

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

本文保留原文的七种继承方式:原型链继承、借用构造函数、组合继承、原型式继承、寄生式继承、寄生组合继承和 ES6 extends。原文中的页面代码块格式标记、代码粘连、object(Chinese) 拼写错误、重复调用父构造函数以及把 class 简单说成“语法糖”等内容已修正或补充现代边界。

原文:JS 总结之原型继承的几种方式

一、先准备父类

下面的构造函数用于说明传统写法。age ?? 18 会在传入 0 时保留 0,比原文的 age || 18 更准确:

function Person(age) {
  this.age = age ?? 18
}

Person.prototype.sleep = function () {
  return 'sleeping'
}

JavaScript 没有强制的“类继承关键字”才能建立继承关系。常见做法是让子类实例的 [[Prototype]] 连接到父类的 prototype,并在构造阶段复用父类初始化逻辑。

需要区分三种关系:

实例 → 子类.prototype → 父类.prototype → Object.prototype → null

未显式建立静态继承时:
子类构造器 → Function.prototype → Object.prototype → null

使用 class extends 或本文 inherit() 建立静态继承时:
子类构造器 → 父类构造器 → Function.prototype → Object.prototype → null

prototype 是构造函数用于创建实例的属性;实例的真实内部原型是 [[Prototype]],读取它应使用 Object.getPrototypeOf()

二、方式 1:原型链继承

function Programmer() {}

Programmer.prototype = new Person()
Programmer.prototype.constructor = Programmer
Programmer.prototype.code = function () {
  return 'coding'
}

const jon = new Programmer()
console.log(jon.code()) // coding
console.log(jon.sleep()) // sleeping
console.log(jon instanceof Person) // true
console.log(jon instanceof Programmer) // true
console.log(Object.getPrototypeOf(jon) === Programmer.prototype) // true

优点:实现简单,实例可以访问父类原型上的方法。

缺点:

  1. new Person() 发生在设置原型时,不能为每个 Programmer 实例传递不同的父类构造参数;
  2. 父类构造器创建的引用类型属性会被所有子类实例共享;
  3. Programmer.prototype 被替换后必须手动恢复 constructor
  4. 创建一个额外的父类实例作为原型可能产生无意义的初始化副作用。

共享状态的例子:

function PersonWithTags() {
  this.tags = []
}

function ProgrammerWithTags() {}
ProgrammerWithTags.prototype = new PersonWithTags()
ProgrammerWithTags.prototype.constructor = ProgrammerWithTags

const first = new ProgrammerWithTags()
const second = new ProgrammerWithTags()
first.tags.push('js')

console.log(second.tags) // ['js']:引用被共享

三、方式 2:借用构造函数(经典继承)

借用构造函数只在子类构造器中调用父类构造器:

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

const jon = new Programmer(18, 'jon')
console.log(jon.name, jon.age) // jon 18
console.log(jon.sleep) // undefined:没有继承 Person.prototype
console.log(jon instanceof Person) // false
console.log(jon instanceof Programmer) // true

优点:

  • 可以向父类构造器传参;
  • 父类构造器创建的自有属性属于当前实例,不会直接在实例之间共享。

缺点:

  • 没有建立 Programmer.prototypePerson.prototype 的关系;
  • 不能访问父类原型方法;
  • 如果父类把方法写在构造器中,每次调用都会创建一份新函数。

原文把父构造器临时挂到 this.newMethod 再删除,这种“对象冒充”写法可以理解历史实现,但现代代码应直接使用 Person.call(this, age),避免可见的临时属性和异常时未清理的问题。

四、方式 3:组合继承

组合继承把“借用构造函数”和“原型链继承”组合起来:

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

// 这里会调用一次 Person,设置 Programmer.prototype 的初始属性
Programmer.prototype = new Person()
Programmer.prototype.constructor = Programmer
Programmer.prototype.code = function () {
  return 'coding'
}

const jon = new Programmer(18, 'jon')
const flash = new Programmer(22, 'flash')

console.log(jon.age, jon.name) // 18 jon
console.log(flash.age, flash.name) // 22 flash
console.log(jon.sleep()) // sleeping
console.log(jon instanceof Person) // true
console.log(jon instanceof Programmer) // true

它解决了前两种方式的一部分问题,但父类构造器被调用两次:设置 Programmer.prototype 时调用一次,创建每个实例时又调用一次。父类自有属性可能先被写入子类原型,再被实例自有属性遮蔽,因此存在冗余。

五、方式 4:原型式继承

原型式继承直接让新对象以已有对象为原型:

const person = {
  name: 'Ada',
  skills: []
}

const jon = Object.create(person)
const flash = Object.create(person)

jon.name = 'Jon' // 自有属性,不影响 person.name
jon.skills.push('JavaScript') // 修改的是共享的数组

console.log(jon.name) // Jon
console.log(flash.name) // Ada
console.log(flash.skills) // ['JavaScript']
console.log(Object.getPrototypeOf(jon) === person) // true

原文的 create(o) 空构造函数实现:

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

const object = { value: 1 }
const copy = create(object)
console.log(copy.value) // 1
console.log(Object.getPrototypeOf(copy) === object) // true

现代代码优先使用 Object.create(prototype),因为它直接表达意图,也可以创建 Object.create(null) 这样的无原型对象。

注意:Object.create 不是深复制,也不是把原对象的所有属性复制到新对象;新对象只是通过原型链访问原对象的属性。引用类型属性仍可能共享。

六、方式 5:寄生式继承

寄生式继承在原型式继承得到的对象上增加新能力,再返回该对象:

function createProgrammer(prototype, name) {
  const programmer = Object.create(prototype)

  programmer.name = name
  programmer.code = function () {
    return `${this.name} is coding`
  }

  return programmer
}

const programmer = createProgrammer(Person.prototype, 'Jon')
console.log(programmer.code()) // Jon is coding
console.log(programmer.sleep()) // sleeping

缺点是每次调用工厂函数都会创建新的 code 函数;如果方法可以共享,应放到一个明确的原型对象或 class 方法中。寄生式继承也不会自动提供构造器身份,programmer instanceof Person 是否成立取决于传入的原型链。

七、方式 6:寄生组合继承

寄生组合继承只调用一次父类构造器:

  • 子类构造器通过 Person.call(this, age) 复制父类自有状态;
  • 子类原型通过 Object.create(Person.prototype) 连接到父类原型;
  • 不再使用 new Person() 作为子类原型,因此没有多余的父类初始化。
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 Programmer(age, name) {
  Person.call(this, age)
  this.name = name
}

inherit(Programmer, Person)
Programmer.prototype.code = function () {
  return 'coding'
}

const jon = new Programmer(18, 'jon')
console.log(jon.name, jon.age) // jon 18
console.log(jon.sleep()) // sleeping
console.log(jon.code()) // coding
console.log(jon instanceof Person) // true
console.log(jon instanceof Programmer) // true
console.log(Object.getPrototypeOf(Programmer) === Person) // true

这是传统构造函数中较完整的实现,但仍需要手动维护 super 调用、静态继承、属性描述符和异常边界。原文引用《JavaScript 高级程序设计》称它是引用类型的理想继承范式,这个评价属于 ES5 时代的历史背景;现代项目通常优先使用 class 或组合。

八、方式 7:ES6 class extends

class PersonClass {
  constructor(age = 18) {
    this.age = age
  }

  sleep() {
    return 'sleeping'
  }
}

class ProgrammerClass extends PersonClass {
  constructor(age, name) {
    super(age)
    this.name = name
  }

  code() {
    return 'coding'
  }
}

const jon = new ProgrammerClass(18, 'jon')
console.log(jon.name, jon.age) // jon 18
console.log(jon.sleep()) // sleeping
console.log(jon.code()) // coding
console.log(jon instanceof PersonClass) // true
console.log(jon instanceof ProgrammerClass) // true
console.log(Object.getPrototypeOf(ProgrammerClass.prototype) === PersonClass.prototype) // true
console.log(Object.getPrototypeOf(ProgrammerClass) === PersonClass) // true

class 使用原型方法和构造器,但不是把 JavaScript 变成 Java/C++ 那种基于类的运行时。class 具有自己的语义:

  • class 声明默认处于严格模式;
  • class 构造器不能被普通调用,必须使用 new
  • 派生构造器必须先调用 super() 才能访问 this
  • 方法默认不可枚举;
  • extends 同时建立实例原型链和构造器静态原型链;
  • 私有字段 #field、静态初始化块等能力不能简单等同于 ES5 的函数拷贝。

原文说“只要部分浏览器支持,需要转为 ES5”是历史兼容性说明。现代目标浏览器和 Node.js 普遍支持 class;是否转译应由项目的目标环境、构建配置和兼容性需求决定,而不是因为 class 本身不可靠。

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

如果只是想让一个对象继承另一个对象,不需要人为创建空构造函数:

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

原文的空构造函数版本可以改成:

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

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

console.log(Doctor.nation) // 中国

现代代码推荐 Object.create。如果对象需要封装状态、校验输入、共享方法或定义明确的 API,工厂函数、class 或组合通常比直接构造长原型链更清晰。

十、继承不等于必须使用继承

继承适合稳定的“is-a”关系,但业务代码经常更适合组合(has-a):

function canCode(target) {
  return {
    code() {
      return `${target.name} is coding`
    }
  }
}

function createDeveloper(name) {
  const developer = { name }
  Object.assign(developer, canCode(developer))
  return developer
}

console.log(createDeveloper('Ada').code()) // Ada is coding

组合可以减少脆弱的基类依赖和深层原型链。选择继承方式时,应考虑:共享行为的位置、状态是否隔离、是否需要 instanceof、是否需要静态继承,以及跨 Realm/序列化边界。

十一、总结

方式主要优点主要问题现代建议
原型链继承简单,可继承原型方法引用属性共享,不能灵活传参仅用于理解历史代码
借用构造函数可传参,实例状态隔离没有原型方法继承作为底层步骤理解
组合继承兼顾状态和方法父构造器调用两次了解 ES5 代码即可
原型式继承直接基于对象创建对象引用属性可能共享使用 Object.create
寄生式继承可对原型对象进行增强方法可能重复创建谨慎使用
寄生组合继承ES5 中效率和语义较完整手写细节多维护旧代码时使用
class extends语义清晰,支持 super/私有字段仍需理解原型和构造语义新代码的常用选择
组合低耦合,能力可复用需要设计对象协作业务代码优先考虑

参考资料

457 DOCUMENTS · 10 COLLECTIONS
ARCHIVE SEARCH457 篇文章

SEARCH GUIDE

输入关键词开始搜索

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

按分类浏览

10 COLLECTIONS