JavaScript 继承全方案详解:从原型链到 Class 语法

13 阅读5分钟

JavaScript 继承全方案详解:从原型链到 Class 语法

前言:为什么需要继承?

让我们从一个现实场景开始:

function User(name) {
  this.name = name
}
User.prototype.login = function() {
  console.log(`${this.name} logged in`)
}

// 现在需要创建 Admin 用户,拥有 User 的所有特性外加特殊权限
// 如何避免重复代码?这就是继承要解决的问题!

JavaScript 作为一门基于原型的语言,提供了多种实现继承的方式,每种方式都有其特点和适用场景。

1. 原型链继承

基本实现

function Parent() {
  this.name = 'Tom'
  this.like = [1, 2]
}

function Child(age) {
  this.age = age
}

Child.prototype = new Parent()

const child1 = new Child(18)
const child2 = new Child(20)

特点分析

  • 优点

    • 简单直观
    • 父类原型方法可被继承
  • 缺点

    1. 多个实例共享同一个原型对象,引用类型属性会相互影响
      child1.like.push(3)
      console.log(child2.like) // [1, 2, 3]
      
    2. 子类无法向父类构造函数传参
      // 无法在创建 Child 时初始化 Parent 的 name
      

内存图示

child1 → Parent实例(作为原型) → Parent.prototype
child2 ↗

2. 构造函数继承(经典继承)

基本实现

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

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

function Child(name) {
  Parent.call(this, name)  // 关键步骤
  this.age = 18
}

const c1 = new Child('Tom')

特点分析

  • 优点

    • 解决了原型链继承的两个问题:
      1. 每个实例都有独立的属性
      2. 可以向父类传参
  • 缺点

    1. 无法继承父类原型上的方法
      console.log(c1.getName) // undefined
      
    2. 方法只能在构造函数中定义,无法复用

适用场景

  • 只需要继承实例属性时
  • 不关心父类原型方法的情况

3. 组合继承(伪经典继承)

基本实现

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

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

function Child(name) {
  Parent.call(this, name)  // 第二次调用 Parent
  this.age = 18
}

Child.prototype = new Parent()  // 第一次调用 Parent
Child.prototype.constructor = Child

const c1 = new Child('Tom')
console.log(c1.getName()) // 'Tom'

特点分析

  • 优点

    • 结合了原型链和构造函数的优点
    • 实例属性独立,可继承原型方法
  • 缺点

    1. 父类构造函数被调用两次,导致:
      • 子类原型上有多余的属性
      • 性能开销

内存图示

c1 → 
  |- 自身属性: name, age
  |- __proto__ → Parent实例(包含多余的name属性) → Parent.prototype

4. 原型式继承

基本实现

const person = {
  name: 'Tom',
  friends: ['Alice', 'Bob']
}

const p1 = Object.create(person)
p1.friends.push('Charlie')

const p2 = Object.create(person)
console.log(p2.friends) // ['Alice', 'Bob', 'Charlie']

特点分析

  • 优点

    • 简洁的对象继承
    • 不需要构造函数
  • 缺点

    1. 与原型链继承相同,引用类型属性共享
    2. 无法实现代码复用(没有构造函数)

Object.create() 的关系

// 等效于
function object(o) {
  function F() {}
  F.prototype = o
  return new F()
}

适用场景

  • 简单对象继承
  • 不需要构造函数的场景

5. 寄生组合式继承(终极方案)

基本实现

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

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

// 关键步骤1:建立原型链连接
Child.prototype = Object.create(Parent.prototype)
// 关键步骤2:修复构造函数指向
Child.prototype.constructor = Child

function Child(name) {
  // 关键步骤3:调用父类构造函数
  Parent.call(this, name)
  this.age = 18
}

let c1 = new Child('Tom')

代码解析:三步实现完美继承

第一步:原型链连接
Child.prototype = Object.create(Parent.prototype)
  • 作用:创建一个以 Parent.prototype 为原型的新对象,作为 Child.prototype

  • 优势

    • 避免了 new Parent() 带来的多余属性问题
    • 保持了纯净的原型链
  • 对比原型链继承

    • 原型链继承:Child.prototype = new Parent()(会在原型上创建不必要的实例属性)
    • 寄生组合式:只继承原型方法,不包含实例属性
第二步:修复构造函数
Child.prototype.constructor = Child
  • 为什么需要

    • 上一步操作会导致 Child.prototype.constructor 指向 Parent
    • 这步修正确保实例的 constructor 属性正确指向 Child
  • 重要性

    • 保持构造函数引用的正确性
    • 某些库会依赖这个属性
第三步:调用父类构造函数
Parent.call(this, name)
  • 作用

    • 在子类实例上初始化父类的属性
    • 确保每个实例都有独立的属性副本
  • 优势

    • 解决了原型链继承的属性共享问题
    • 可以向父类传递参数

内存结构图解

c1 实例:

{
  name: 'Tom',      // 来自 Parent.call(this, name)
  age: 18,          // 子类自有属性
  __proto__: Child.prototype {
    constructor: Child,
    __proto__: Parent.prototype {
      getName: function...,
      __proto__: Object.prototype
    }
  }
}

6. ES6 Class 继承

基本实现

class Parent {
  constructor(name) {
    this.name = name
  }
  
  getName() {
    return this.name
  }
}

class Child extends Parent {
  constructor(name) {
    super(name)  // 相当于 Parent.call(this, name)
    this.age = 18
  }
}

const c = new Child('Tom')

特点分析

  • 优点

    • 语法简洁
    • 底层使用寄生组合式继承
    • 内置 super 关键字访问父类
  • 注意事项

    1. 必须先调用 super() 才能使用 this
    2. class 方法是不可枚举的
    3. 本质仍是原型继承的语法糖

Babel 转译结果

// 实际会被转译为类似寄生组合继承的代码

对比总结

继承方式原型方法实例属性独立避免重复调用语法简洁性
原型链继承⭐⭐
构造函数继承⭐⭐
组合继承⭐⭐⭐
原型式继承N/A⭐⭐⭐
寄生组合式继承
Class 继承⭐⭐⭐⭐

面试常见问题

Q1: 各种继承方式的优缺点?

回答策略

  1. 按顺序介绍每种方式
  2. 重点对比原型链/构造函数/组合继承
  3. 最终引出寄生组合是最佳方案
  4. 说明 class 是语法糖

Q2: 实现一个继承函数?

function extend(Child, Parent) {
  Child.prototype = Object.create(Parent.prototype)
  Child.prototype.constructor = Child
  // 可选:保存父类引用
  Child.super = Parent.prototype
}

Q3: super 关键字的原理?

回答要点

  1. 在构造函数中指向父类构造函数
  2. 在方法中指向父类原型
  3. 内部通过 [[HomeObject]] 实现

最佳实践建议

  1. 现代项目:直接使用 class 语法
  2. 旧代码维护:使用寄生组合式继承
  3. 避免
    • 直接修改 __proto__
    • 深度继承链(通常3层以上就难以维护)
  4. 注意
    • 继承会增加代码耦合度
    • 考虑组合优于继承的原则

终极方案代码

// 最健壮的继承实现
function inherit(Child, Parent) {
  // 创建原型对象
  const prototype = Object.create(Parent.prototype, {
    constructor: {
      value: Child,
      enumerable: false,
      writable: true,
      configurable: true
    }
  })
  
  // 设置静态属性继承
  if (Object.setPrototypeOf) {
    Object.setPrototypeOf(Child, Parent)
  } else {
    Child.__proto__ = Parent
  }
  
  Child.prototype = prototype
}

// 使用示例
function Parent(name) {
  this.name = name
}

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

function Child(name, age) {
  Parent.call(this, name)
  this.age = age
}

inherit(Child, Parent)

理解 JavaScript 继承机制是成为高级开发者的必经之路。掌握这些知识后,你将能够:

  • 在面试中游刃有余
  • 阅读框架源码更轻松
  • 设计更合理的对象体系
  • 避免常见的继承陷阱