JavaScript的继承

121 阅读6分钟

一、什么是继承?

如果 A对象 能够访问 B对象 的属性和方法,说明 A对象 继承自 B对象,并且将 A对象 称为 子类,将 B对象 称为 父类

JavaScript继承的核心 —— 原型链:
每个 JavaScript 对象都有一个内置属性 [[Prototype]] (可以通过 __proto__Object.getPrototypeOf()访问),指向它的原型对象。当访问一个对象的属性或方法时,如果该对象自身没有该属性或方法,JavaScript引擎会沿着 原型链 向上查找,直到找到该属性或方法,或者到达原型链的末端(null

二、继承的方式

1. 原型链继承

(1) 实现方式:

// 原型链继承
// 父类
function Person(name, age) {
  this.name = name
  this.age = age
}

Person.prototype.sayHello = function () {
  console.log('hello')
}

// 子类
function Student(gender) {
  this.gender = gender
}

// 实现继承
Student.prototype = new Person('张三', 18)

const s1 = new Student('男')
console.log('s1', s1)                 // s1 Person { gender: '男' }
console.log('s1.name', s1.name)       // s1.name 张三
console.log('s1.gender', s1.gender)   // s1.gender 男
s1.sayHello()                         // hello

const s2 = new Student('女')         
console.log('s2', s2)                 // s2 Person { gender: '女' }
console.log('s2.name', s2.name)       // s2.name 张三
console.log('s2.gender', s2.gender)   // s2.gender 女
s2.sayHello()

(2) 优点:

  • 实现简单,父类原型上的属性和方法可以被所有实例共享

(3) 缺点:

  • 子类的实例不能向父类构造函数传参
  • 所有子类实例对象共用一个原型对象,如果一个子类实例修改了父类原型上的属性,那么其他子类实例也会受到影响

2. 构造函数继承

(1) 实现方式:

// 构造函数继承
// 父类
function Person() {
    this.name = '张三';
}

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

// 子类
function Student(name) {
    Person.call(this,name);
    this.type = 'student';
}

const s1 = new Student();
console.log('s1', s1);    // s1 Student { name: '张三', type: 'student' }
s1.getName();             // 报错,s1中没有getName方法

(2) 优点:

  • 可以向父类构造函数传递参数
  • 每个子类实例拥有独立的父类属性副本,避免了原型链继承中子类实例共享父类原型属性的问题

(3) 缺点:

  • 无法继承父类原型上的方法,只能继承父类实例上的属性和方法,方法必须定义在构造函数中(无法复用)

3. 组合继承

(1) 实现方式:

// 组合式继承
// 父类
function Person(name, age) {
  this.name = name
  this.age = age
}

Person.prototype.sayHello = function () {
  console.log('hello')
}

// 子类
function Student(name, age, gender) {
  Person.call(this, name, age)
  this.gender = gender
}

// 利用原型链继承
Student.prototype = new Person()

const s1 = new Student('张三', 18, '男')
console.log('s1',s1);                       // s1 Person { name: '张三', age: 18, gender: '男' }
s1.sayHello();                              // hello

const s2 = new Student('李四', 19, '女')   
console.log('s2',s2);                      // s2 Person { name: '李四', age:19, gender: '女' }
s2.sayHello();                             // hello

(2) 优点:

  • 父类的属性和方法都可以继承,每一个继承下来的属性都是自己的独立私有属性

(3) 缺点:

  • 父类的属性和方法都继承了两次,一次是在子类的原型上(new Person()),一次是在子类的实例上(Person.call(this,name,age))

4. 原型式继承

(1) 实现方式:

// 原型式继承

function createObject(obj) {
  function F() {}
  F.prototype = obj
  return new F()
}

const person = {
  name: '张三',
  hobbies: ['篮球', '游泳'],
  sayHello: function() {
    console.log('你好,我是' + this.name)
  }
}

const s1 = createObject(person)
s1.name = '李四'
s1.hobbies.push('阅读')

const s2 = createObject(person)

console.log(s1.name)                      // 李四(实例属性)
console.log(s2.name)                      // 张三(原型属性)
console.log(s1.hobbies)                   // ['篮球', '游泳', '阅读'](共享引用属性)
console.log(s2.hobbies)                   // ['篮球', '游泳', '阅读'](共享引用属性)
console.log(s1.sayHello === s2.sayHello)  // true(共享引用属性)

(2) 优点:

  • 解决了组合继承中父类构造函数被调用两次的问题
  • 父类方法可以复用

(3) 缺点:

  • 引用类型的属性被所有实例共享(和原型链继承相同的问题)
  • 无法向父类传参
  • 实例之间的属性和方法不共享

5. 寄生式继承

(1) 实现方式:

// 寄生式继承
function createEnhancedObject(obj) {
  const clone = Object.create(obj)            // 使用原型式继承创建新对象
  // 添加新方法增强对象
  clone.sayHello = function() {
    console.log('你好,我是' + this.name)
  }
  return clone
}

const person = {
  name: '张三',
  age: 18
}

const student = createEnhancedObject(person)
student.sayHello()                            // 你好,我是张三
console.log(student.age)                      // 18

(2) 优点:

  • 可以创建一个与原对象有相同原型的对象
  • 可以在创建对象后,对对象进行增强,添加新的属性和方法

(3) 缺点:

  • 每个对象都会创建新的方法副本,与构造函数模式类似
  • 引用类型的属性被所有实例共享(和原型链继承相同的问题)

6. 寄生组合式继承

(1) 实现方式:

// 寄生组合式继承

function inheritPrototype(Child, Parent) {
  // 创建父类原型的副本
  const prototype = Object.create(Parent.prototype)
  // 修复构造函数指向
  prototype.constructor = Child
  // 将副本设置为子类的原型
  Child.prototype = prototype
}

// 父类
function Person(name, age) {
  this.name = name
  this.age = age
}

Person.prototype.sayHello = function() {
  console.log('你好,我是' + this.name)
}

// 子类
function Student(name, age, grade) {
  Person.call(this, name, age)                 // 继承属性
  this.grade = grade
}

// 继承方法
inheritPrototype(Student, Person)

// 添加子类特有方法
Student.prototype.study = function() {
  console.log(this.name + '正在学习')
}

const s1 = new Student('张三', 18, '大一')
s1.sayHello()                                  // 你好,我是张三
s1.study()                                     // 张三正在学习

const s2 = new Student('李四', 19, '大二')
s2.sayHello()                                  // 你好,我是李四
s2.study()                                     // 李四正在学习
console.log(s1.sayHello === s2.sayHello)       // true
console.log(s1.study === s2.study)             // true
console.log(s1.constructor === Student && s2.constructor === Student) // true

(2) 优点:

  • 完美解决原型链继承和构造函数继承的缺点
  • 只调用一次父类构造函数
  • 原型链保持干净,没有多余的属性和方法

(3) 缺点:

  • 实现复杂,需要额外的辅助函数
  • 需要手动修复构造函数指向

7. ES6类继承

(1) 实现方式:

// ES6类继承

class Person {
  constructor(name, age) {
    this.name = name
    this.age = age
  }
  
  sayHello() {
    console.log(`你好,我是${this.name}`)
  }
}

class Student extends Person {
  constructor(name, age, grade) {
    super(name, age)  // 调用父类构造函数
    this.grade = grade
  }
  
  study() {
    console.log(`${this.name}正在学习${this.grade}的课程`)
  }
  
  // 方法重写
  sayHello() {
    super.sayHello()  // 调用父类方法
    console.log(`我是${this.grade}的学生`)
  }
}

const s1 = new Student('张三', 18, '大一')
s1.sayHello()                                     // 你好,我是张三
                                                  // 我是大一的学生
s1.study()                                        // 张三正在学习大一的课程
const s2 = new Student('李四', 19, '大二')
s2.sayHello()                                     // 你好,我是李四
                                                  // 我是大二的学生
s2.study()                                        // 李四正在学习大二的课程
console.log(s1.sayHello === s2.sayHello)          // true
console.log(s1.study === s2.study)                // true
console.log(s1.constructor === Student && s2.constructor === Student) // true

(2) 优点:

  • 语法简洁明了,更符合面向对象编程习惯
  • 内置 super 关键字,方便调用父类方法
  • 底层实现基于寄生组合式继承

(3) 缺点:

  • 需要现代JavaScript环境支持(ES6+)
  • 类中定义的方法无法被枚举
  • 必须先调用 super() 才能使用 this

三、总结

继承方式核心思想优点缺点适用场景
原型链继承子类原型指向父类实例简单易实现引用属性共享,无法传参简单继承场景
构造函数继承子类中调用父类构造函数实例属性独立,可传参无法继承父类原型链上的方法,方法需要定义在构造函数内部(无法复用)需要属性隔离的场景
组合继承构造函数+原型链继承属性独立,方法共享父类构造函数调用两次常规继承场景
原型式继承基于现有对象创建新对象简单对象继承引用属性共享对象克隆/简单继承
寄生式继承在原型式基础上增强对象可添加新方法方法无法复用需要扩展对象的场景
寄生组合继承通过空函数桥接父类原型高效,无冗余属性实现复杂高性能要求的继承场景
ES6类继承extends 和 super 语法糖语法简洁,现代标准需要ES6+环境支持现代开发首选