JavaScript继承深度解析:从ES5到ES6的全方位指南
在JavaScript的面向对象编程中,继承是一个核心概念。它不仅能够帮助我们减少重复代码,更是实现多态的重要前提。本文将从ES5的原型链继承讲起,逐步深入到ES6的class语法糖,带你全面理解JavaScript中的继承机制。
📖 目录
一、引言:JavaScript面向对象与继承的重要性 {#引言}
1.1 面向对象的三大特性
面向对象编程有三大特性:封装 、继承 、多态。
- 封装:将属性和方法封装到一个类中,实现数据和行为的组织
- 继承:让子类直接使用父类的属性和方法,减少重复代码
- 多态:不同对象在执行相同操作时表现出不同的行为
1.2 为什么继承如此重要?
在软件开发中,我们经常需要创建多个相似但又有差异的类。比如:
javascript
// 没有继承时:大量重复代码
class Dog {
constructor(name, age) {
this.name = name;
this.age = age;
}
eat() { console.log('狗狗吃东西'); }
sleep() { console.log('狗狗睡觉'); }
bark() { console.log('汪汪汪'); }
}
class Cat {
constructor(name, age) {
this.name = name;
this.age = age;
}
eat() { console.log('猫咪吃东西'); } // 重复代码
sleep() { console.log('猫咪睡觉'); } // 重复代码
meow() { console.log('喵喵喵'); }
}
通过继承,我们可以将这些共同的特性抽取到父类中,子类只需要关注自己独有的部分。
1.3 JavaScript继承的特殊性
重要提示 :JavaScript的继承与Java、C++等传统面向对象语言有本质区别。JavaScript使用的是原型链继承 机制,而不是类继承。即使ES6引入了class关键字,它也仅仅是构造函数和原型链的语法糖。
理解这一点非常重要------只有深入理解原型链,才能真正掌握JavaScript的继承机制。
二、ES5中的继承实现 {#es5继承}
2.1 理解原型链基础
在深入继承之前,我们需要先理解JavaScript中对象和函数的原型机制。
2.1.1 对象的原型 [[prototype]]
JavaScript中每个对象都有一个特殊的内置属性 [[prototype]],这个属性可以指向另一个对象。
javascript
// 字面量方式创建对象
const obj = { name: '张三' };
// 获取对象原型的两种方式
// 方式一:通过 __proto__ 属性(不推荐,存在兼容性问题)
console.log(obj.__proto__);
// 方式二:通过 Object.getPrototypeOf 方法(推荐)
console.log(Object.getPrototypeOf(obj));
内存表现:
┌─────────────────────────────────────────┐
│ obj 对象 │
│ ┌─────────────────────────────────┐ │
│ │ name: "张三" │ │
│ │ [[Prototype]] ────────────────┼─┼──┐ │
│ └─────────────────────────────────┘ │ │
│ │ │
│ ┌─────────────────────────────────┐ │ │
│ │ Object.prototype 对象 │◄──┘ │
│ │ toString: ƒ │ │
│ │ valueOf: ƒ │ │
│ │ hasOwnProperty: ƒ │ │
│ │ ... │ │
│ └─────────────────────────────────┘ │
└─────────────────────────────────────────┘
当访问对象的属性时,如果对象本身没有这个属性,JavaScript会自动沿着 [[prototype]] 链向上查找,直到找到或到达 Object.prototype(其 [[prototype]] 为 null)。
2.1.2 函数的原型 prototype
所有函数都有一个特殊的 prototype 属性,这个属性指向一个对象。
javascript
function Person(name, age) {
this.name = name;
this.age = age;
}
// prototype 是函数特有的属性
console.log(typeof Person.prototype); // "object"
关键点 :prototype 不是因为函数是对象才有的,而是因为它是一个函数才有的这个属性。
2.1.3 constructor 属性
原型对象上有一个默认的 constructor 属性,它指向构造函数本身:
javascript
function Person(name, age) {
this.name = name;
this.age = age;
}
console.log(Person.prototype.constructor === Person); // true
如果我们重写原型对象,需要手动修复 constructor 指向:
javascript
// 错误做法:constructor 指向 Object
Person.prototype = {
constructor: Person, // 需要手动修复
sayHello: function() {
console.log(`Hello, I'm ${this.name}`);
}
};
// 推荐做法:使用 Object.defineProperty
Object.defineProperty(Person.prototype, 'constructor', {
enumerable: false,
configurable: true,
writable: true,
value: Person
});
2.1.4 new 操作符的原理
使用 new 关键字调用构造函数时,会执行以下步骤:
javascript
function Person(name) {
this.name = name;
}
const p = new Person('张三');
执行过程:
- 在内存中创建一个新的空对象
- 这个对象的
[[prototype]]被赋值为构造函数的prototype属性 - 构造函数内部的
this指向新创建的对象 - 执行构造函数内部的代码
- 返回新创建的对象(除非构造函数显式返回另一个对象)
内存图示:
┌────────────────────────────────────────────────────────────┐
│ │
│ function Person() {} │
│ │ │
│ ▼ │
│ Person.prototype ──────────────────┐ │
│ │ │ │
│ │ constructor ─────────────┤ │
│ │ │ │
│ ▼ ▼ │
│ ┌─────────────────┐ ┌─────────────────┐ │
│ │ 原型对象 │ │ p 对象 │ │
│ │ (0xa00) │ │ (0x200) │ │
│ │ │ │ │ │
│ │ │◄──┐ │ name: "张三" │ │
│ │ │ │ │ [[Prototype]]───┘ │
│ └─────────────────┘ │ └─────────────────┘ │
│ │ │
└─────────────────────────┼───────────────────────────────────┘
2.2 原型链继承(Prototype Chain Inheritance)
2.2.1 实现原理
原型链继承的核心思想是:让子类的原型对象等于父类的实例。
javascript
// 定义父类
function Person(name, age) {
this.name = name;
this.age = age;
}
Person.prototype.running = function() {
console.log(`${this.name} is running`);
};
Person.prototype.eating = function() {
console.log(`${this.name} is eating`);
};
// 定义子类
function Student(sno) {
this.sno = sno;
}
// 核心:让子类的原型等于父类的实例
Student.prototype = new Person();
// 修复子类的 constructor 指向
Student.prototype.constructor = Student;
// 给子类添加独有的方法
Student.prototype.studying = function() {
console.log(`${this.name} is studying, 学号: ${this.sno}`);
};
// 测试
const stu = new Student('张三', 18, '001');
stu.running(); // "张三 is running"
stu.eating(); // "张三 is eating"
stu.studying(); // "张三 is studying, 学号: 001"
2.2.2 内存结构图
┌────────────────────────────────────────────────────────────────────┐
│ 原型链继承内存结构 │
├────────────────────────────────────────────────────────────────────┤
│ │
│ function Person() function Student() │
│ │ │ │
│ ▼ ▼ │
│ Person.prototype ──→ Student.prototype │
│ ▲ │ │
│ │ new Person() │ │
│ │ ▼ │
│ │ ┌─────────────────┐ │
│ │ │ Person 实例对象 │ │
│ │ │ (作为原型) │ │
│ │ │ │ │
│ │ │ name: undefined │ │
│ │ │ age: undefined │ │
│ │ │ [[Prototype]]────┼────→ Person.prototype │
│ │ │ running: ƒ │ │
│ │ │ eating: ƒ │ │
│ │ └─────────────────┘ │
│ │ │
│ ┌──────┴───────┐ ┌─────────────────┐ │
│ │Person.prototype│ │ Student 实例 │ │
│ │ │◄──┐ │ │ │
│ │ constructor ───┘ │ │ sno: "001" │ │
│ │ running: ƒ │ │ [[Prototype]]─────┘ │
│ │ eating: ƒ │ │ │ │
│ └───────────────────┘ │ studying: ƒ │ │
│ ▲ └─────────────────┘ │
│ │ │
│ ┌──────┴───────┐ │
│ │ Object.proto │ │
│ │ │ │
│ │ toString: ƒ │ │
│ │ ... │ │
│ └───────────────┘ │
│ ▲ │
│ │ │
│ [Object: null prototype] {} │
│ │
└────────────────────────────────────────────────────────────────────┘
2.2.3 原型链查找顺序
当访问 stu.running 时,JavaScript会按照以下顺序查找:
- 在
stu对象本身查找 → 没找到 - 在
stu.__proto__(即Student.prototype)查找 → 没找到 - 在
stu.__proto__.__proto__(即Person.prototype)查找 → 找到了!
2.2.4 原型链继承的弊端
⚠️ 原型链继承存在三个重要问题:
javascript
// 问题一:引用类型属性被多个实例共享
function Parent() {
this.hobbies = ['读书', '运动'];
}
function Child() {}
Child.prototype = new Parent();
Child.prototype.constructor = Child;
const child1 = new Child();
child1.hobbies.push('编程');
console.log(child1.hobbies); // ['读书', '运动', '编程']
const child2 = new Child();
console.log(child2.hobbies); // ['读书', '运动', '编程'] ⚠️ 被污染了!
javascript
// 问题二:无法给父类构造函数传递参数
function Person(name) {
this.name = name;
}
function Student(sno) {
this.sno = sno;
}
Student.prototype = new Person(); // name 是 undefined
Student.prototype.constructor = Student;
const stu = new Student('001');
console.log(stu.name); // undefined ⚠️ 无法获取到参数
javascript
// 问题三:创建子类实例时,无法初始化父类属性
const stu = new Student('002');
// 父类的 name 和 age 属性无法被正确初始化
2.2.5 应用场景
原型链继承适用于:
- 场景一:需要创建大量对象,且这些对象共享相同的方法
- 场景二:父类和子类的属性都是基本类型,不需要在构造函数中定制
- 场景三:原型链继承是其他继承方式的基础
2.3 借用构造函数继承(Constructor Stealing)
2.3.1 实现原理
借用构造函数继承(也称为经典继承 或伪造对象 )的核心思想是:在子类构造函数内部调用父类构造函数。
javascript
// 定义父类
function Person(name, age) {
this.name = name;
this.age = age;
this.hobbies = ['读书', '运动'];
}
Person.prototype.running = function() {
console.log(`${this.name} is running`);
};
// 定义子类
function Student(name, age, sno) {
// 核心:在子类构造函数中调用父类构造函数
Person.call(this, name, age); // 或 Person.apply(this, arguments)
this.sno = sno;
}
// 继承父类的原型方法(但通过原型链方式)
Student.prototype = new Person();
Student.prototype.constructor = Student;
// 添加子类独有的方法
Student.prototype.studying = function() {
console.log(`${this.name} (学号: ${this.sno}) 正在学习`);
};
// 测试
const stu1 = new Student('张三', 18, '001');
stu1.hobbies.push('编程');
console.log(stu1.hobbies); // ['读书', '运动', '编程']
const stu2 = new Student('李四', 20, '002');
console.log(stu2.hobbies); // ['读书', '运动'] ⚠️ 引用类型问题已解决!
stu1.running(); // "张三 is running" ⚠️ 父类方法可以调用
2.3.2 call 和 apply 的区别
javascript
function Person(name, age) {
this.name = name;
this.age = age;
}
function Student(name, age, sno) {
// call 方式:逐个传递参数
Person.call(this, name, age);
// apply 方式:传递参数数组
// Person.apply(this, [name, age]);
// 特殊用法:传递 arguments 对象
// Person.apply(this, arguments);
this.sno = sno;
}
2.3.3 借用构造函数继承的优缺点
优点 :
✅ 解决了引用类型属性被共享的问题
✅ 可以在子类构造函数中向父类传递参数
✅ 每个子类实例都有独立的父类属性副本
缺点 :
❌ 父类的方法无法复用(每个实例都会创建一份新的方法)
❌ 父类的原型方法对子类实例不可见
❌ 函数调用了两次(一次在继承时,一次在创建实例时)
javascript
// 问题演示:父类方法不可复用
function Person(name) {
this.name = name;
// 每次 new Person 都会创建新的 sayHello 函数
this.sayHello = function() {
console.log(`Hello, I'm ${this.name}`);
};
}
function Student(name, sno) {
Person.call(this, name);
this.sno = sno;
}
const stu = new Student('张三', '001');
// stu.sayHello 是独立函数,不是共享的方法
2.4 组合继承(Combination Inheritance)
2.4.1 实现原理
组合继承是JavaScript中最常用的继承模式,结合了原型链继承和借用构造函数继承的优点。
javascript
// 定义父类
function Person(name, age) {
this.name = name;
this.age = age;
this.hobbies = ['读书', '运动'];
}
Person.prototype.running = function() {
console.log(`${this.name} is running`);
};
Person.prototype.eating = function() {
console.log(`${this.name} is eating`);
};
// 定义子类
function Student(name, age, sno) {
// 第二次调用父类构造函数:为每个实例初始化独立的属性
Person.call(this, name, age);
this.sno = sno;
}
// 第一次调用父类构造函数:创建父类实例作为子类原型
Student.prototype = new Person();
Student.prototype.constructor = Student;
// 添加子类独有的方法
Student.prototype.studying = function() {
console.log(`${this.name} (学号: ${this.sno}) 正在学习`);
};
// 测试
const stu1 = new Student('张三', 18, '001');
const stu2 = new Student('李四', 20, '002');
// 验证引用类型独立性
stu1.hobbies.push('编程');
console.log(stu1.hobbies); // ['读书', '运动', '编程']
console.log(stu2.hobbies); // ['读书', '运动'] ✓ 独立
// 验证原型方法可访问
stu1.running(); // "张三 is running" ✓
stu1.studying(); // "张三 (学号: 001) 正在学习" ✓
2.4.2 组合继承的内存结构
┌────────────────────────────────────────────────────────────────────┐
│ 组合继承内存结构 │
├────────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────┐ ┌─────────────────┐ │
│ │ Student 实例 │ │ Student.prototype │ │
│ │ stu1 (0x200) │ │ (Person 实例) │ │
│ │ │ │ │ │
│ │ name: "张三" │ │ name: undefined │ ←─┐ │
│ │ age: 18 │ │ age: undefined │ ←─┤ 第一次 │
│ │ hobbies: [...] │ │ hobbies: [...] │ ←─┤ 调用 │
│ │ sno: "001" │ │ [[Prototype]]────┼─►│ Person │
│ │ [[Prototype]]───┼──────┐ │ running: ƒ │ │ │
│ └─────────────────┘ │ │ eating: ƒ │ │ │
│ │ │ studying: ƒ │ │
│ │ └─────────────────┘ │
│ │ ▲ │
│ └────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────┐ │
│ │ Person.prototype │ │
│ │ │ │
│ │ constructor ─────┤ │
│ │ running: ƒ │ │
│ │ eating: ƒ │ │
│ └─────────────────┘ │
│ │
│ 问题:name、age、hobbies 在两处存在! │
│ - Student.prototype 上有一份(undefined/[...]) │
│ - stu1/stu2 实例上有一份("张三"/18/["读书"...]) │
│ │
└────────────────────────────────────────────────────────────────────┘
2.4.3 组合继承的问题
⚠️ 组合继承最大的问题是:父类构造函数被调用了两次。
- 第一次:
Student.prototype = new Person() - 第二次:
Person.call(this, name, age)
这导致:
- 父类属性在原型对象上有一份(浪费内存)
- 父类属性在子类实例上也有一份
- 虽然访问时会优先读取实例上的属性,但原型上的属性确实存在
2.5 原型式继承(Prototypal Inheritance)
2.5.1 渊源
原型式继承由道格拉斯·克罗克福德(Douglas Crockford,JSON的创立者)在2006年提出。
2.5.2 实现原理
原型式继承的核心思想是:通过一个临时构造函数,让新对象的原型指向已有对象。
javascript
// 原始原型式继承函数
function createObject(o) {
function F() {}
F.prototype = o;
return new F();
}
// 现代等价实现:Object.create()
const person = {
name: '张三',
age: 18,
hobbies: ['读书', '运动'],
running: function() {
console.log(`${this.name} is running`);
}
};
const student = Object.create(person);
student.name = '李四';
student.sno = '001';
console.log(student.name); // "李四"(自有属性)
console.log(student.hobbies); // ['读书', '运动'](继承属性)
student.running(); // "李四 is running"
console.log(student.__proto__ === person); // true
2.5.3 Object.create 的 Polyfill
javascript
if (!Object.create) {
Object.create = function(o) {
function F() {}
F.prototype = o;
return new F();
};
}
2.5.4 应用场景
原型式继承适用于:
- 只需要继承已有对象的属性和方法
- 不需要创建"类"
- 类似于浅拷贝,但保持了原型链关系
javascript
// 场景:创建多个相似对象
const animalBase = {
type: '动物',
diet: '杂食',
sleep: function() {
console.log(`${this.name} 在睡觉`);
}
};
const dog = Object.create(animalBase);
dog.name = '旺财';
dog.bark = function() {
console.log('汪汪汪');
};
const cat = Object.create(animalBase);
cat.name = '咪咪';
cat.meow = function() {
console.log('喵喵喵');
};
dog.sleep(); // "旺财 在睡觉"
cat.sleep(); // "咪咪 在睡觉"
2.6 寄生式继承(Parasitic Inheritance)
2.6.1 实现原理
寄生式继承是原型式继承的增强版,结合了工厂模式的思想:创建一个封装继承过程的函数,在内部增强对象,最后返回。
javascript
// 寄生式继承函数
function createStudent(person, sno) {
// 通过原型式继承创建对象
const student = Object.create(person);
// 增强对象:添加子类特有的属性和方法
student.sno = sno;
student.studying = function() {
console.log(`${this.name} (学号: ${this.sno}) 正在学习`);
};
return student;
}
// 基类对象
const person = {
name: '默认姓名',
age: 0,
running: function() {
console.log(`${this.name} is running`);
}
};
// 使用寄生式继承创建学生对象
const stu1 = createStudent(person, '001');
const stu2 = createStudent(person, '002');
stu1.name = '张三';
stu1.studying(); // "张三 (学号: 001) 正在学习"
stu2.name = '李四';
stu2.studying(); // "李四 (学号: 002) 正在学习"
2.6.2 寄生式继承的改进版本
javascript
function createStudent(person, name, age, sno) {
// 原型式继承
const student = Object.create(person);
// 增强:添加/覆盖属性
student.name = name;
student.age = age;
student.sno = sno;
// 增强:添加方法
student.studying = function() {
console.log(`${this.name} (学号: ${this.sno}) 正在学习`);
};
student.takeExam = function() {
console.log(`${this.name} 正在参加考试`);
};
return student;
}
const personProto = {
running: function() {
console.log(`${this.name} is running`);
},
eating: function() {
console.log(`${this.name} is eating`);
}
};
const stu = createStudent(personProto, '王五', 20, '003');
stu.running(); // "王五 is running"
stu.eating(); // "王五 is eating"
stu.studying(); // "王五 (学号: 003) 正在学习"
stu.takeExam(); // "王五 正在参加考试"
2.6.3 寄生式继承的优缺点
优点 :
✅ 可以在继承时增强对象,添加额外的方法和属性
✅ 代码封装性好,易于理解
✅ 灵活性高,可以根据需要定制继承行为
缺点 :
❌ 方法需要在每次继承时重新创建,无法复用
❌ 增强的方法没有放在原型上,浪费内存
❌ 与构造函数模式类似,存在函数重复创建的问题
2.7 寄生组合式继承(Parasitic Combination Inheritance)⭐
2.7.1 为什么需要寄生组合式继承?
回顾组合继承的问题:
- 父类构造函数被调用两次:一次创建子类原型,一次创建实例
- 父类属性存在两份:一份在原型上,一份在实例上
寄生组合式继承正是为了解决这两个问题。
2.7.2 实现原理
核心思想:不再让子类的原型等于父类的实例,而是等于父类原型的副本。
javascript
// 寄生组合式继承的核心函数
function inheritPrototype(SubType, SuperType) {
// 创建父类原型对象的副本
const prototype = Object.create(SuperType.prototype);
// 修复 constructor 指向
prototype.constructor = SubType;
// 将副本设置为子类原型
SubType.prototype = prototype;
}
// 定义父类
function Person(name, age) {
this.name = name;
this.age = age;
this.hobbies = ['读书', '运动'];
}
Person.prototype.running = function() {
console.log(`${this.name} is running`);
};
Person.prototype.eating = function() {
console.log(`${this.name} is eating`);
};
// 定义子类
function Student(name, age, sno) {
// 只在实例创建时调用一次父类构造函数
Person.call(this, name, age);
this.sno = sno;
}
// 使用寄生组合式继承
inheritPrototype(Student, Person);
// 添加子类独有的方法
Student.prototype.studying = function() {
console.log(`${this.name} (学号: ${this.sno}) 正在学习`);
};
// 测试
const stu1 = new Student('张三', 18, '001');
const stu2 = new Student('李四', 20, '002');
// 验证引用类型独立性
stu1.hobbies.push('编程');
console.log(stu1.hobbies); // ['读书', '运动', '编程']
console.log(stu2.hobbies); // ['读书', '运动'] ✓ 独立
// 验证原型方法可访问
stu1.running(); // "张三 is running"
stu1.studying(); // "张三 (学号: 001) 正在学习"
// 验证原型链正确
console.log(stu1 instanceof Student); // true
console.log(stu1 instanceof Person); // true
console.log(stu1 instanceof Object); // true
2.7.3 内存结构对比
组合继承的内存结构:
Student.prototype → Person实例(有name,age,hobbies) → Person.prototype
↑
浪费内存:name/age/hobbies 重复存储
寄生组合式继承的内存结构:
Student.prototype → Person.prototype副本(无name/age/hobbies) → Person.prototype
2.7.4 寄生组合式继承的优势
✅ 只调用一次父类构造函数 :性能最优
✅ 原型属性不重复 :避免内存浪费
✅ 原型链完整 :instanceof 和 isPrototypeOf 正常工作
✅ 原型方法可复用:方法定义在原型上,所有实例共享
2.7.5 完整的寄生组合式继承封装
javascript
/**
* 寄生组合式继承 - 完整封装
* @param {Function} SubType 子类构造函数
* @param {Function} SuperType 父类构造函数
*/
function inheritPrototype(SubType, SuperType) {
// 1. 创建父类原型对象的副本
const prototype = Object.create(SuperType.prototype);
// 2. 添加 constructor 属性,防止丢失
prototype.constructor = SubType;
// 3. 将副本赋值给子类原型
SubType.prototype = prototype;
}
// 父类
function Animal(name) {
this.name = name;
this.colors = ['black', 'white'];
}
Animal.prototype.move = function() {
console.log(`${this.name} is moving`);
};
// 子类
function Dog(name, breed) {
Animal.call(this, name);
this.breed = breed;
}
// 继承
inheritPrototype(Dog, Animal);
// 子类方法
Dog.prototype.bark = function() {
console.log(`${this.name} is barking: 汪汪汪`);
};
// 测试
const dog1 = new Dog('旺财', '金毛');
const dog2 = new Dog('咪咪', '柯基');
dog1.colors.push('brown');
console.log(dog1.colors); // ['black', 'white', 'brown']
console.log(dog2.colors); // ['black', 'white'] ✓ 独立
dog1.move(); // "旺财 is moving"
dog1.bark(); // "旺财 is barking: 汪汪汪"
2.8 ES5继承方式对比总结
| 继承方式 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| 原型链继承 | 实现简单,方法可复用 | 引用类型共享,无法传参 | 简单场景 |
| 借用构造函数 | 可传参,引用类型独立 | 方法不可复用 | 需要定制属性 |
| 组合继承 | 综合两者优点 | 调用两次构造函数 | 最常用 |
| 原型式继承 | 简洁,无需构造函数 | 方法不可复用 | 快速对象创建 |
| 寄生式继承 | 可增强对象 | 方法不可复用 | 定制继承对象 |
| 寄生组合式继承 | 性能最优,结构清晰 | 实现稍复杂 | 最佳实践 |
三、ES6中的继承实现 {#es6继承}
3.1 class 语法糖的本质
ES6引入了 class 关键字,但它仅仅是JavaScript构造函数和原型链的语法糖。
javascript
// ES6 class 写法
class Person {
constructor(name, age) {
this.name = name;
this.age = age;
}
running() {
console.log(`${this.name} is running`);
}
eating() {
console.log(`${this.name} is eating`);
}
}
// 等价的 ES5 构造函数写法
function Person(name, age) {
this.name = name;
this.age = age;
}
Person.prototype.running = function() {
console.log(`${this.name} is running`);
};
Person.prototype.eating = function() {
console.log(`${this.name} is eating`);
};
3.1.1 class 的声明方式
javascript
// 方式一:类声明(不能提升,类似 const/let)
class Person {
constructor(name) {
this.name = name;
}
}
// 方式二:类表达式
const Animal = class {
constructor(type) {
this.type = type;
}
};
// 方式三:命名类表达式
const Vehicle = class Car {
constructor(brand) {
this.brand = brand;
}
};
3.1.2 class 的特性
javascript
class Person {
constructor(name, age) {
this.name = name;
this.age = age;
}
// 实例方法 - 定义在原型上
sayHello() {
console.log(`Hello, I'm ${this.name}`);
}
// 静态方法 - 定义在类本身
static create(name, age) {
return new Person(name, age);
}
// getter 和 setter
get info() {
return `${this.name}, ${this.age}岁`;
}
set info(value) {
const [name, age] = value.split(',');
this.name = name;
this.age = parseInt(age);
}
}
const p = Person.create('张三', 18);
p.sayHello(); // "Hello, I'm 张三"
console.log(p.info); // "张三, 18岁"
p.info = '李四, 20';
console.log(p.info); // "李四, 20岁"
3.1.3 class 的本质验证
javascript
class Person {
constructor(name) {
this.name = name;
}
}
console.log(typeof Person); // "function"
console.log(Person === Person.prototype.constructor); // true
// 所有方法都定义在 prototype 上
console.log(Person.prototype.sayHello); // undefined(没有定义)
3.2 extends 关键字的使用
ES6使用 extends 关键字实现继承,比ES5的实现简洁得多。
3.2.1 基础用法
javascript
// 父类
class Person {
constructor(name, age) {
this.name = name;
this.age = age;
}
running() {
console.log(`${this.name} is running`);
}
eating() {
console.log(`${this.name} is eating`);
}
}
// 子类使用 extends 继承父类
class Student extends Person {
constructor(name, age, sno) {
// 调用父类构造函数
super(name, age); // 必须先调用 super
this.sno = sno;
}
studying() {
console.log(`${this.name} (学号: ${this.sno}) 正在学习`);
}
}
// 测试
const stu = new Student('张三', 18, '001');
stu.running(); // "张三 is running" - 继承自父类
stu.eating(); // "张三 is eating" - 继承自父类
stu.studying(); // "张三 (学号: 001) 正在学习" - 子类自有
3.2.2 extends 的原型链关系
javascript
class Person {}
class Student extends Person {}
console.log(Student.prototype.__proto__ === Person.prototype); // true
const stu = new Student();
console.log(stu.__proto__.__proto__ === Person.prototype); // true
console.log(stu instanceof Student); // true
console.log(stu instanceof Person); // true
3.2.3 继承的内存结构(ES6)
┌─────────────────────────────────────────────────────────────────┐
│ ES6 Class 继承内存结构 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ class Person class Student │
│ │ │ │
│ ▼ ▼ │
│ Person.prototype ──────────────► Student.prototype │
│ ▲ │ │
│ │ │ extends │
│ │ ▼ │
│ ┌──────┴───────┐ ┌─────────────────┐ │
│ │ Person.prototype│ │ [[Prototype]]──┼──→ Person │
│ │ │ │ constructor ─────┤ prototype │
│ │ constructor ──┼──────┐ │ │ │
│ │ │ │ │ studying: ƒ │ │
│ └──────┬───────┘ │ └─────────────────┘ │
│ │ │ ▲ │
│ │ │ │ │
│ ▼ │ │ │
│ ┌───────────────┐ │ │ │
│ │Object.prototype│◄───┴────────────────────┘ │
│ │ │ │
│ └───────────────┘ │
│ ▲ │
│ │ │
│ [Object: null prototype] {} │
│ │
└─────────────────────────────────────────────────────────────────┘
3.3 super 关键字的三种使用场景
super 是ES6继承中最重要的关键字,它有三种使用场景。
3.3.1 在构造函数中使用 super
javascript
class Parent {
constructor(name, age) {
this.name = name;
this.age = age;
}
}
class Child extends Parent {
constructor(name, age, school) {
// 必须先调用 super,才能使用 this
super(name, age);
this.school = school;
}
}
const child = new Child('张三', 10, '第一小学');
console.log(child.name); // "张三"
console.log(child.age); // 10
console.log(child.school); // "第一小学"
⚠️ 重要规则:
- 在子类构造函数中,必须先调用
super()才能使用this - 如果定义了构造函数,必须调用
super() - 如果不调用
super(),JavaScript会报错
javascript
// 错误示例
class Child extends Parent {
constructor(name, age) {
// 报错:Must call super constructor before using 'this'
this.name = name;
this.age = age;
}
}
3.3.2 在实例方法中使用 super
javascript
class Person {
constructor(name) {
this.name = name;
}
greet() {
return `你好,我是 ${this.name}`;
}
}
class Student extends Person {
constructor(name, grade) {
super(name);
this.grade = grade;
}
// 在实例方法中使用 super 调用父类方法
greet() {
// 调用父类的 greet 方法
const parentGreeting = super.greet();
return `${parentGreeting},我是 ${this.grade} 年级学生`;
}
}
const student = new Student('小明', 5);
console.log(student.greet());
// "你好,我是 小明,我是 5 年级学生"
3.3.3 在静态方法中使用 super
javascript
class Person {
static create(name, age) {
return new Person(name, age);
}
}
class Student extends Person {
static create(name, age, grade) {
// 调用父类的静态方法
const person = super.create(name, age);
person.grade = grade;
return person;
}
}
const student = Student.create('张三', 15, 3);
console.log(student instanceof Student); // true
console.log(student.grade); // 3
3.3.4 super 使用总结
| 使用场景 | 语法 | 说明 |
|---|---|---|
| 构造函数 | super(args) |
调用父类构造函数,必须先于 this 调用 |
| 实例方法 | super.method(args) |
调用父类的实例方法 |
| 静态方法 | super.staticMethod(args) |
调用父类的静态方法 |
3.4 继承内置类
ES6允许我们继承JavaScript的内置类(如Array、Map、Set等)。
3.4.1 继承 Array
javascript
// 创建一个只包含偶数的数组类
class EvenArray extends Array {
constructor(...args) {
// 过滤出偶数
const evenArgs = args.filter(n => n % 2 === 0);
super(...evenArgs);
}
// 添加元素时自动过滤奇数
push(...items) {
const evenItems = items.filter(n => n % 2 === 0);
super.push(...evenItems);
}
}
const evenNums = new EvenArray(1, 2, 3, 4, 5, 6);
console.log(evenNums); // [2, 4, 6]
console.log(evenNums.length); // 3
evenNums.push(7, 8, 9);
console.log(evenNums); // [2, 4, 6, 8] - 7和9被过滤
// 继承原生方法
console.log(evenNums.filter(n => n > 3)); // [4, 6, 8]
console.log(evenNums.map(n => n * 2)); // [4, 8, 12, 16]
3.4.2 继承 Map
javascript
class PersistentMap extends Map {
constructor() {
super();
this.storage = [];
}
set(key, value) {
super.set(key, value);
this.storage.push({ key, value });
return this;
}
getHistory() {
return this.storage;
}
}
const map = new PersistentMap();
map.set('name', '张三');
map.set('age', 18);
console.log(map.get('name')); // "张三"
console.log(map.getHistory()); // [{key: 'name', value: '张三'}, {key: 'age', value: 18}]
3.4.3 继承 Error
javascript
class CustomError extends Error {
constructor(message, statusCode) {
super(message);
this.name = 'CustomError';
this.statusCode = statusCode;
// 修复 V8 原型链的堆栈追踪
if (Error.captureStackTrace) {
Error.captureStackTrace(this, CustomError);
}
}
}
try {
throw new CustomError('出错了!', 500);
} catch (e) {
console.log(e.message); // "出错了!"
console.log(e.statusCode); // 500
console.log(e.name); // "CustomError"
}
3.5 混入(Mixin)实现多继承
JavaScript的类只支持单继承(只有一个父类)。当我们需要在一个类中添加多个类的功能时,可以使用Mixin模式。
3.5.1 Mixin 的基本实现
javascript
// 定义两个 Mixin
const FlyMixin = {
fly() {
console.log(`${this.name} is flying`);
}
};
const SwimMixin = {
swim() {
console.log(`${this.name} is swimming`);
}
};
const WalkMixin = {
walk() {
console.log(`${this.name} is walking`);
}
};
// Mixin 函数
function mixin(target, ...mixins) {
Object.assign(target.prototype, ...mixins);
}
// 创建类
class Animal {
constructor(name) {
this.name = name;
}
}
// 混入多个Mixin的功能
mixin(Animal, FlyMixin, SwimMixin, WalkMixin);
const duck = new Animal('鸭子');
duck.fly(); // "鸭子 is flying"
duck.swim(); // "鸭子 is swimming"
duck.walk(); // "鸭子 is walking"
3.5.2 ES6 Class Mixin 写法
javascript
// 第一个Mixin
const Flyable = (SuperClass) => class extends SuperClass {
fly() {
console.log(`${this.name} is flying`);
}
};
// 第二个Mixin
const Swimmable = (SuperClass) => class extends SuperClass {
swim() {
console.log(`${this.name} is swimming`);
}
};
// 第三个Mixin
const Walkable = (SuperClass) => class extends SuperClass {
walk() {
console.log(`${this.name} is walking`);
}
};
// 多重继承:通过组合多个Mixin
class Animal {
constructor(name) {
this.name = name;
}
}
// 从右到左依次混入
class Duck extends Walkable(Swimmable(Flyable(Animal))) {
quack() {
console.log(`${this.name} is quacking: 嘎嘎嘎`);
}
}
const duck = new Duck('唐老鸭');
duck.fly(); // "唐老鸭 is flying"
duck.swim(); // "唐老鸭 is swimming"
duck.walk(); // "唐老鸭 is walking"
duck.quack(); // "唐老鸭 is quacking: 嘎嘎嘎"
3.5.3 多个Mixin的场景示例
javascript
// 可绑定的Mixin
const Bindable = (Base) => class extends Base {
bind(event, handler) {
if (!this._handlers) this._handlers = {};
if (!this._handlers[event]) this._handlers[event] = [];
this._handlers[event].push(handler);
return this;
}
trigger(event, data) {
if (this._handlers && this._handlers[event]) {
this._handlers[event].forEach(handler => handler(data));
}
return this;
}
};
// 可序列化的Mixin
const Serializable = (Base) => class extends Base {
toJSON() {
const obj = {};
for (const key in this) {
if (this.hasOwnProperty(key)) {
obj[key] = this[key];
}
}
return obj;
}
static fromJSON(json) {
return Object.assign(new this(), json);
}
};
// 可验证的Mixin
const Validatable = (Base) => class extends Base {
validate(rules) {
for (const [field, validator] of Object.entries(rules)) {
if (!validator(this[field])) {
throw new Error(`Validation failed for ${field}`);
}
}
return true;
}
};
// 组合使用
class User extends Validatable(Serializable(Bindable(Object))) {
constructor(name, email) {
super();
this.name = name;
this.email = email;
}
}
const user = new User('张三', 'zhangsan@example.com');
user.bind('save', () => console.log('用户已保存'));
user.trigger('save');
const json = user.toJSON();
console.log(json);
const restored = User.fromJSON(json);
console.log(restored.name); // "张三"
四、JavaScript中的多态 {#多态}
4.1 什么是多态?
多态(Polymorphism)是面向对象编程的三大特性之一。维基百科的定义:
多态指为不同数据类型的实体提供统一的接口,或使用一个单一的符号来表示多个不同的类型。
简单理解:不同对象在执行相同操作时,表现出不同的行为。
4.2 JavaScript中的多态实现
JavaScript天然支持多态,因为它的弱类型和动态类型特性。
4.2.1 基于继承的多态
javascript
class Shape {
area() {
throw new Error('子类必须重写 area() 方法');
}
}
class Circle extends Shape {
constructor(radius) {
super();
this.radius = radius;
}
area() {
return Math.PI * this.radius * this.radius;
}
}
class Rectangle extends Shape {
constructor(width, height) {
super();
this.width = width;
this.height = height;
}
area() {
return this.width * this.height;
}
}
class Triangle extends Shape {
constructor(base, height) {
super();
this.base = base;
this.height = height;
}
area() {
return 0.5 * this.base * this.height;
}
}
// 多态:同一个函数处理不同类型的对象
function printArea(shape) {
console.log(`面积: ${shape.area()}`);
}
const circle = new Circle(5);
const rectangle = new Rectangle(4, 6);
const triangle = new Triangle(3, 4);
printArea(circle); // "面积: 78.53981633974483"
printArea(rectangle); // "面积: 24"
printArea(triangle); // "面积: 6"
4.2.2 基于参数类型的多态
javascript
function format(value) {
if (typeof value === 'string') {
return value.toUpperCase();
} else if (typeof value === 'number') {
return value.toFixed(2);
} else if (Array.isArray(value)) {
return value.join(', ');
} else if (typeof value === 'object') {
return JSON.stringify(value);
}
return String(value);
}
console.log(format('hello')); // "HELLO"
console.log(format(3.14159)); // "3.14"
console.log(format([1, 2, 3])); // "1, 2, 3"
console.log(format({a: 1})); // "{"a":1}"
4.2.3 多态的实际应用
javascript
class Animal {
speak() {
throw new Error('子类必须实现 speak() 方法');
}
}
class Dog extends Animal {
speak() {
return '汪汪汪';
}
}
class Cat extends Animal {
speak() {
return '喵喵喵';
}
}
class Cow extends Animal {
speak() {
return '哞哞哞';
}
}
// 农场主给所有动物喂食
function feedAnimals(animals) {
animals.forEach(animal => {
console.log(`喂食: ${animal.speak()}`);
});
}
const animals = [
new Dog(),
new Cat(),
new Cow(),
new Dog()
];
feedAnimals(animals);
// 喂食: 汪汪汪
// 喂食: 喵喵喵
// 喂食: 哞哞哞
// 喂食: 汪汪汪
五、TypeScript中的继承扩展 {#typescript}
5.1 TypeScript 类的基本继承
TypeScript在ES6的基础上增强了类型系统。
typescript
// 基类
class Person {
protected name: string;
protected age: number;
constructor(name: string, age: number) {
this.name = name;
this.age = age;
}
greet(): string {
return `你好,我是 ${this.name}`;
}
}
// 子类
class Student extends Person {
private grade: number;
constructor(name: string, age: number, grade: number) {
super(name, age);
this.grade = grade;
}
greet(): string {
return `${super.greet()},我是 ${this.grade} 年级学生`;
}
}
const student = new Student('张三', 15, 3);
console.log(student.greet()); // "你好,我是 张三,我是 3 年级学生"
5.2 访问修饰符
| 修饰符 | 类自身 | 子类 | 其他 |
|---|---|---|---|
public |
✅ | ✅ | ✅ |
protected |
✅ | ✅ | ❌ |
private |
✅ | ❌ | ❌ |
readonly |
✅ | ✅ | ✅ (仅读) |
typescript
class Base {
public publicProp = 'public';
protected protectedProp = 'protected';
private privateProp = 'private';
readonly readonlyProp = 'readonly';
}
class Derived extends Base {
accessProps() {
console.log(this.publicProp); // ✅ OK
console.log(this.protectedProp); // ✅ OK
// console.log(this.privateProp); // ❌ 错误
console.log(this.readonlyProp); // ✅ OK
}
}
const instance = new Base();
console.log(instance.publicProp); // ✅ OK
// console.log(instance.protectedProp); // ❌ 错误
// console.log(instance.privateProp); // ❌ 错误
5.3 抽象类
typescript
abstract class Shape {
abstract area(): number; // 抽象方法,子类必须实现
describe(): string {
return `面积: ${this.area()}`;
}
}
class Circle extends Shape {
constructor(private radius: number) {
super();
}
area(): number {
return Math.PI * this.radius ** 2;
}
}
class Rectangle extends Shape {
constructor(private width: number, private height: number) {
super();
}
area(): number {
return this.width * this.height;
}
}
const shapes: Shape[] = [new Circle(5), new Rectangle(4, 6)];
shapes.forEach(shape => console.log(shape.describe()));
// "面积: 78.53981633974483"
// "面积: 24"
5.4 接口继承类
typescript
class Point {
x: number;
y: number;
constructor(x: number, y: number) {
this.x = x;
this.y = y;
}
}
interface Point3D extends Point {
z: number;
}
class Point3DImpl implements Point3D {
x: number;
y: number;
z: number;
constructor(x: number, y: number, z: number) {
this.x = x;
this.y = y;
this.z = z;
}
}
六、总结与最佳实践 {#总结}
6.1 ES5 vs ES6 继承对比
| 特性 | ES5 | ES6 |
|---|---|---|
| 实现方式 | 原型链 + 构造函数 | class + extends |
| 代码量 | 较多,需要手动处理原型链 | 简洁,语法糖 |
| 内存效率 | 组合继承有冗余 | 寄生组合式更优 |
| 可读性 | 需要理解原型链机制 | 更接近传统OOP |
| 兼容性 | 所有浏览器 | 现代浏览器(IE不支持) |
6.2 最佳实践建议
6.2.1 现代项目推荐
javascript
// ✅ 推荐:使用 ES6 class
class Animal {
constructor(name) {
this.name = name;
}
speak() {
console.log(`${this.name} makes a noise.`);
}
}
class Dog extends Animal {
speak() {
console.log(`${this.name} barks.`);
}
}
6.2.2 需要兼容老项目时
javascript
// ⚠️ 兼容老项目的寄生组合式继承
function Animal(name) {
this.name = name;
}
Animal.prototype.speak = function() {
console.log(`${this.name} makes a noise.`);
};
function Dog(name, breed) {
Animal.call(this, name);
this.breed = breed;
}
Dog.prototype = Object.create(Animal.prototype);
Dog.prototype.constructor = Dog;
Dog.prototype.speak = function() {
console.log(`${this.name} barks.`);
};
6.3 常见坑点和注意事项
6.3.1 super 调用顺序
javascript
// ❌ 错误:先使用 this
class Child extends Parent {
constructor(name) {
this.name = name; // 报错
super(name); // 必须在 this 之前调用
}
}
// ✅ 正确:先调用 super
class Child extends Parent {
constructor(name) {
super(name); // 先调用
this.name = name; // 后使用 this
}
}
6.3.2 方法覆盖和 super 调用
javascript
class Parent {
method() {
console.log('Parent method');
}
}
class Child extends Parent {
method() {
super.method(); // 调用父类方法
console.log('Child method');
}
}
6.3.3 静态方法继承
javascript
class Parent {
static create(name) {
return new Parent(name);
}
}
class Child extends Parent {}
console.log(Child.create('test') instanceof Child); // true
6.4 继承方式选择指南
┌─────────────────────────────────────────────────────────────────┐
│ 继承方式选择决策树 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 是否需要兼容 IE/老浏览器? │
│ │ │
│ ├── 是 → 使用 ES5 寄生组合式继承 │
│ │ │
│ └── 否 → 是否需要继承多个类? │
│ │ │
│ ├── 是 → 使用 Mixin 模式 │
│ │ │
│ └── 否 → 使用 ES6 class extends │
│ │
└─────────────────────────────────────────────────────────────────┘
6.5 最终建议
- 现代项目(ES6+) :直接使用
class extends语法,简洁直观 - 需要兼容性 :使用寄生组合式继承
inheritPrototype() - 多继承需求:使用 Mixin 模式
- 内置类扩展 :使用
extends继承 Array/Map/Error 等 - TypeScript 项目:充分利用类型系统和访问修饰符
📚 参考资料
- MDN Web Docs - 继承与原型链
- ECMAScript 2015 Specification
- 《JavaScript高级程序设计》(第4版)
- 《你不知道的JavaScript》(上卷)
💡 提示:理解JavaScript继承的最佳方式是动手实践。建议读者将本文中的代码示例在浏览器控制台或Node.js环境中实际运行一遍,通过调试器观察原型链的变化,这样才能真正掌握继承的精髓。