面试官:请手写多种JS继承方式 | 一文带你从实现到原理,彻底搞懂继承

102 阅读5分钟

1 什么是继承

JavaScript 中的继承是一种对象间共享属性和方法的机制。与基于类的语言(如 Java、C++)不同,JavaScript 采用基于原型(Prototype) 的继承模型,这是其最核心的特征之一。

1.1 原型继承的本质

在 JavaScript 中,每个对象都有一个内部属性 [[Prototype]](可通过 __proto__Object.getPrototypeOf() 访问),它指向另一个对象(即其原型)。当访问对象的属性时,如果对象自身没有该属性,JavaScript 引擎会沿着原型链向上查找,直到找到该属性或到达链的末端(null)。

1.2 发展历程

想要详细了解的宝子可以去看这篇文章:从 Object.create 到 class:JS 原型的进化论原型 JS 是一门基于原型的语言,其他语言通过类描述和 - 掘金

JavaScript 继承机制经历了从简单到复杂、从功能实现到语法糖封装的发展过程:

  1. 早期阶段:仅通过构造函数和原型实现简单的继承
  2. 模式探索期:开发者创造了多种继承模式(原型链、构造函数、组合继承等)
  3. ES5 规范化Object.create() 方法提供了标准化的原型继承
  4. ES6 现代化:引入 classextends 关键字,提供更直观的类式继承语法

2 实现方式详解

2.1 原型链继承

原理:通过将子类的原型对象指向父类的实例,实现原型链的连接。

function Parent() {
  this.name = 'parent'
  this.like = [12, 13, 14]
}

function Child() {
  this.age = 19
}

// 核心:建立原型链
Child.prototype = new Parent()

let c = new Child()
let d = new Child()
c.like.push(199)
console.log(d.like) // [12, 13, 14, 199] - 共享引用属性

特点

  • 实例共享原型上的引用属性(如数组、对象)
  • 无法向父类构造函数传参
  • 简单易实现,但存在共享问题

2.2 构造函数继承

原理:在子类构造函数中通过 call()apply() 调用父类构造函数。

function Parent(name) {
  this.name = name || "parent"
  this.like = [12, 13, 14]
}

function Child() {
  // 核心:借用父类构造函数
  Parent.call(this, "child") // 可传参
  this.age = 19
}

let c = new Child()
let d = new Child()
c.like.push(199)
console.log(d.like) // [12, 13, 14] - 不共享

特点

  • 解决了引用属性共享问题
  • 可以向父类传参
  • 无法继承父类原型上的属性和方法

2.3 组合式继承

原理:结合原型链继承和构造函数继承的优点。

function Parent(name) {
  this.name = name || "parent"
  this.like = [12, 13, 14]
}

Parent.prototype.getName = function() {
  console.log(this.name)
}

function Child(name) {
  // 第二次调用 Parent
  Parent.call(this, name) // 继承实例属性
  this.age = 19
}

// 第一次调用 Parent
Child.prototype = new Parent() // 继承原型方法
Child.prototype.constructor = Child // 修复构造函数指向

let c = new Child("child1")
let d = new Child("child2")
c.like.push(199)
console.log(d.like) // [12, 13, 14] - 不共享
c.getName() // child1 - 可访问原型方法

特点

  • 结合了两种继承方式的优点
  • 实例属性不共享,原型方法可复用
  • 父类构造函数被调用两次,效率较低

2.4 原型式继承

原理:基于现有对象创建新对象,使用 Object.create() 方法。

const parent = {
  name: 'parent',
  like: ['one', 'two']
}

// 核心:以 parent 为原型创建新对象
const child = Object.create(parent)
console.log(child.__proto__ === parent) // true

child.like.push('three')
console.log(parent.like) // ['one', 'two', 'three'] - 共享引用属性

特点

  • 无需构造函数即可实现对象继承
  • 与原型链继承有相同的共享问题
  • ES5 标准化了这种模式

2.5 寄生式继承

原理:在原型式继承基础上增强对象,添加额外方法。

const parent = {
  name: "parent",
  like: ["one", "two"],
}

function createChild(obj) {
  // 原型式继承
  let clone = Object.create(obj)
  // 增强对象
  clone.getLike = function() {
    return this.like
  }
  return clone
}

const child1 = createChild(parent)
console.log(child1.getLike()) // ["one", "two"]
child1.like.push('three')

const child2 = createChild(parent)
console.log(child2.getLike()) // ["one", "two", "three"] - 共享

特点

  • 为对象添加自定义方法
  • 方法不能复用,每个实例都有单独的方法副本
  • 仍然存在引用属性共享问题

2.6 寄生组合式继承

原理:通过寄生方式修复组合继承中父类构造函数被调用两次的问题。

function Parent(name) {
  this.name = name || "parent"
  this.like = ["one", "two"]
}

Parent.prototype.getName = function() {
  console.log(this.name)
}

function Child(name) {
  // 只调用一次 Parent 构造函数
  Parent.call(this, name)
  this.age = '18'
}

// 核心:不调用 Parent 构造函数,直接继承原型
Child.prototype = Object.create(Parent.prototype)
// 修复构造函数指向
Child.prototype.constructor = Child

let child1 = new Child("child1")
child1.getName() // child1
console.log(Child.prototype.constructor === Child) // true
console.log(child1 instanceof Child) // true
console.log(child1 instanceof Parent) // true

特点

  • 只调用一次父类构造函数
  • 原型链保持正确
  • 是 ES6 之前最理想的继承方式

2.7 ES6 类继承

原理:使用 classextends 关键字实现更直观的继承。

class Parent {
  constructor(name) {
    this.name = name || "parent"
    this.like = ["one", "two"]
  }
  
  // 实例方法
  getName() {
    console.log(this.name)
  }
  
  // 静态方法
  static getHello() {
    console.log("hello world!")
  }
}

class Child extends Parent {
  constructor(name) {
    super(name) // 必须先调用 super()
    this.age = "18"
  }
}

// 调用静态方法
Parent.getHello() // "hello world!"

let child1 = new Child("child1")
child1.getName() // "child1"

let child2 = new Child("child2")
child1.like.push("three")
console.log(child2.like) // ["one", "two"] - 不共享

特点

  • 语法更清晰,更接近传统面向对象语言
  • 内置了 super 关键字用于调用父类方法
  • 支持静态方法的继承
  • 本质仍是基于原型的语法糖

3 总结对比

继承方式优点缺点适用场景
原型链继承简单易实现引用属性共享,无法传参简单对象继承
构造函数继承可传参,不共享引用属性无法继承原型方法需要隔离实例属性的场景
组合继承结合两者优点父类构造函数调用两次通用场景
原型式继承无需构造函数共享引用属性对象字面量继承
寄生式继承可增强对象方法不能复用需要添加额外方法的场景
寄生组合继承最优的 ES5方案实现稍复杂需要高效继承的场景
ES6类继承语法清晰,功能完善需要转换工具支持旧浏览器现代 JavaScript 开发