一、什么是继承?
如果 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+环境支持 | 现代开发首选 |