面向对象 class 与继承
用更优雅的方式组织代码结构
🎯 学习目标
学完本章,你将能够:
- 能够使用 class 关键字定义类,包括构造函数、实例方法和静态方法
- 能够使用 extends 和 super 实现继承,理解原型链关系
- 能够使用私有属性(#)封装内部状态,防止外部直接访问
- 能够使用 getter/setter 控制属性的访问和修改
- 能够解释多态的概念,使用方法重写实现不同子类的行为
一段让人头疼的代码
用构造函数写面向对象,代码长什么样?
// 传统构造函数写法
function Person(name, age) {
this.name = name;
this.age = age;
}
Person.prototype.greet = function() {
return `Hi, I'm ${this.name}`;
};
Person.species = "Homo sapiens"; // 静态属性
function Student(name, age, grade) {
Person.call(this, name, age);
this.grade = grade;
}
Student.prototype = Object.create(Person.prototype);
Student.prototype.constructor = Student;
Student.prototype.study = function() {
return `${this.name} is studying`;
};
光是让 Student 继承 Person 就要写 4 行样板代码。prototype、call、Object.create……每个都对初学者不友好。
ES6 的 class 语法就是为了解决这个问题。但别误会——class 不是新机制,只是语法糖,底层依然是原型链。
Part 1: class 基础
先学会用 class 写类,再深入理解它和原型链的关系
开场那段用构造函数写的代码,光是继承就要四行样板代码。class 语法把这些全部藏起来了——constructor 定义初始化逻辑,方法直接写在花括号里,看起来像 Java 又像 Python。但别忘了,底层还是原型链。这节从基本语法开始,把 class 和原型的关系彻底搞清楚。
class 基本语法
对比构造函数,一眼看懂 class
// class 写法
class Person {
constructor(name, age) {
this.name = name;
this.age = age;
}
greet() {
return `Hi, I'm ${this.name}`;
}
}
let person = new Person("张三", 25);
console.log(person.greet()); // "Hi, I'm 张三"
console.log(typeof Person); // "function"
关键事实:
typeof Person是"function"——class 本质就是函数constructor等同于构造函数本体- 方法自动挂到
Person.prototype上 - class 声明不会提升(hoisting)
class 与原型链
语法糖的底层真相

图:class 写法与原型写法的等价关系
【AI 生图提示词】请生成一张中文技术示意图,风格简洁、配色柔和、适合教学: - 画面主体:左右两列对比布局。左列标题"class 写法",右列标题"等价的原型写法",中间用虚线双向箭头标注"完全等价" - 左列内容:一个蓝色圆角矩形,内部显示 class Person { constructor(name) {...} / greet() {...} },下方展示实例化代码 new Person('张三') - 右列内容:一个橙色圆角矩形,内部显示 function Person(name) {...} 和 Person.prototype.greet = function() {...},下方同样展示实例化代码 - 底部:展示原型链查找路径——new Person('张三') → 实例.__proto__ === Person.prototype → Person.prototype.__proto__ === Object.prototype,用绿色节点和箭头连接 - 关键标注:typeof Person === "function"、class 是语法糖、方法自动挂到 prototype 上 - 配色建议:浅蓝/浅灰背景,深色文字,左列蓝色系、右列橙色系、底部绿色节点,等价箭头用橙色虚线标注 - 画面比例:16:9 - 避免:过度装饰、3D 效果、杂乱的颜色
// 证明:class 就是语法糖
class Person { /* ... */ }
console.log(typeof Person); // "function"
console.log(Person === Person.prototype.constructor); // true
实例属性
类字段让属性声明更清晰
class User {
// 类字段(ES2022)
role = "user";
createdAt = new Date();
constructor(name, email) {
this.name = name;
this.email = email;
}
getProfile() {
return `${this.name} (${this.email})`;
}
}
let user = new User("张三", "zhang@test.com");
console.log(user.getProfile()); // "张三 (zhang@test.com)"
console.log(user.role); // "user"
Part 2: 静态成员
不属于实例,属于类本身的属性和方法
new Person() 创建的实例有 name、greet(),但如果有些东西应该是所有实例共享的,而不是每个实例各存一份呢?比如工具方法、配置常量。静态成员就挂载在类本身上,不需要 new 就能调用。这节看看 static 怎么用,以及 ES2022 新增的静态初始化块。
静态方法
不需要 new 就能调用的方法
class MathUtils {
static PI = 3.14159;
static add(a, b) { return a + b; }
static multiply(a, b) { return a * b; }
}
console.log(MathUtils.PI); // 3.14159
console.log(MathUtils.add(1, 2)); // 3
// 静态方法中的 this 指向类本身
class Animal {
static create(name) {
return new this(name); // this === Animal
}
constructor(name) { this.name = name; }
}
let dog = Animal.create("Dog");
new MathUtils().add(1, 2) 会报 TypeError。
静态初始化块
类加载时自动执行一段逻辑
class Config {
static settings;
static {
// 类加载时自动执行
Config.settings = {
env: process.env.NODE_ENV || "development",
port: 3000
};
}
}
console.log(Config.settings.port); // 3000
静态初始化块(ES2022)相当于类的"启动脚本",在类定义被评估时执行一次。
Part 3: 继承
继承让子类复用父类的代码,同时可以扩展或覆盖行为
开场那四行样板代码还记得吗?用 class 的话,一行 extends 就搞定了。子类自动拥有父类的所有能力,还能用自己的版本覆盖。但 super() 的调用时机有个大坑——在 this 之前必须先调 super,否则直接报错。这节把继承的写法、原型链的关系、方法重写一次讲透。
extends 与 super
一行继承,告别原型链样板代码
class Animal {
constructor(name) { this.name = name; }
speak() { return `${this.name} makes a sound`; }
}
class Dog extends Animal {
constructor(name, breed) {
super(name); // 必须在使用 this 之前调用
this.breed = breed;
}
speak() {
return `${this.name} barks!`;
}
parentSpeak() {
return super.speak(); // 调用父类方法
}
}
let dog = new Dog("Buddy", "Golden");
console.log(dog.speak()); // "Buddy barks!"
console.log(dog.parentSpeak()); // "Buddy makes a sound"
console.log(dog instanceof Animal); // true
继承的原型链
extends 背后发生了什么

图:继承原型链 dog 实例的完整查找路径
【AI 生图提示词】请生成一张中文技术示意图,风格简洁、配色柔和、适合教学: - 画面主体:一条从左到右的原型链,由 4 个圆角矩形节点和箭头组成:dog 实例 → Dog.prototype → Animal.prototype → Object.prototype → null - 各节点内容:dog 实例(蓝色,包含 name: "Buddy", breed: "Golden");Dog.prototype(绿色,标注 speak() → 'Buddy barks!');Animal.prototype(橙色,标注 speak() → 'makes a sound' 和 constructor → Animal);Object.prototype(浅灰色,标注 toString、hasOwnProperty 等);null(白色虚线框) - 箭头标注:每条箭头上方标注 __proto__,用灰色小字 - 右侧旁注:用虚线引出补充说明——Dog.prototype.speak 覆盖了 Animal.prototype.speak,实例查找时先找到子类的版本 - 关键标注:instanceof 检查整条链、属性查找沿链向上直到找到或到达 null - 配色建议:浅蓝/浅灰背景,深色文字,dog 实例蓝色、Dog.prototype 绿色、Animal.prototype 橙色、Object.prototype 浅灰色、null 白色虚线,箭头用深灰色 - 画面比例:16:9 - 避免:过度装饰、3D 效果、杂乱的颜色
// 验证原型链
console.log(Object.getPrototypeOf(Dog.prototype) === Animal.prototype);
// true
// 等价的原型写法
// Dog.prototype = Object.create(Animal.prototype);
// Dog.prototype.constructor = Dog;
方法重写
子类用同名方法覆盖父类
class Bird extends Animal {
speak() {
return `${this.name} chirps!`;
}
fly() {
return `${this.name} is flying`;
}
}
let bird = new Bird("Tweety");
console.log(bird.speak()); // "Tweety chirps!"(重写了)
console.log(bird.fly()); // "Tweety is flying"(新增的)
当子类和父类有同名方法时,实例调用的是子类的版本。如果想调用父类版本,用 super.method()。
Part 4: 封装
ES2022 的 # 私有字段终于让 JS 有了真正的私有性
写一个 BankAccount 类,余额字段不想让外部直接改,怎么办?以前只能靠约定(命名加下划线),外部还是能访问。ES2022 的 # 私有字段从语法层面锁死了——外部访问直接报错,不是"君子协定"了。再配合 getter/setter,你就能做到"数据藏起来,接口露出来"的经典封装。
私有属性 #
加个 #,外部彻底不可见
class BankAccount {
#balance = 0; // 私有字段
constructor(owner, initialBalance = 0) {
this.owner = owner;
this.#balance = initialBalance;
}
#validate(amount) { // 私有方法
if (amount <= 0) throw new Error("金额必须大于0");
}
deposit(amount) {
this.#validate(amount);
this.#balance += amount;
}
withdraw(amount) {
this.#validate(amount);
if (amount > this.#balance) throw new Error("余额不足");
this.#balance -= amount;
}
get balance() { return this.#balance; }
}
let acc = new BankAccount("张三", 1000);
acc.deposit(500);
console.log(acc.balance); // 1500
// acc.#balance; // SyntaxError!
getter/setter
用属性的方式调用方法
class Temperature {
#celsius;
constructor(celsius) { this.#celsius = celsius; }
get fahrenheit() { return this.#celsius * 9/5 + 32; }
set fahrenheit(f) { this.#celsius = (f - 32) * 5/9; }
get celsius() { return this.#celsius; }
}
let temp = new Temperature(100);
console.log(temp.fahrenheit); // 212(看起来像属性,实际调了 get)
temp.fahrenheit = 32; // 实际调了 set
console.log(temp.celsius); // 0
getter/setter 的好处:外部代码不知道内部实现。你可以把 #celsius 换成 #kelvin,只要 get/set 接口不变,外部代码一行都不用改。
Part 5: 多态与 SOLID
面向对象不只是写 class,更重要的是设计思想
class、继承、封装这些是"工具"。但什么时候该用继承?什么时候该拆类?怎么让代码改起来不牵一发动全身?多态让你用统一接口处理不同类型,SOLID 五条原则帮你做出更好的设计决策。这些不是理论空谈,而是每天写代码都会遇到的选择。
多态
同一个方法,不同的行为
class Shape {
area() { throw new Error("子类必须实现 area()"); }
describe() { return `面积: ${this.area().toFixed(2)}`; }
}
class Circle extends Shape {
constructor(r) { super(); this.r = r; }
area() { return Math.PI * this.r ** 2; }
}
class Rectangle extends Shape {
constructor(w, h) { super(); this.w = w; this.h = h; }
area() { return this.w * this.h; }
}
// 同一个接口,不同行为——这就是多态
let shapes = [new Circle(5), new Rectangle(4, 6)];
shapes.forEach(s => console.log(s.describe()));
// "面积: 78.54"
// "面积: 24.00"
SOLID 简介
五个设计原则,写出可维护的代码
| 字母 | 原则 | 一句话解释 |
|---|---|---|
| S | 单一职责 | 一个类只做一件事 |
| O | 开闭原则 | 对扩展开放,对修改关闭 |
| L | 里氏替换 | 子类可以替代父类使用 |
| I | 接口隔离 | 不要强迫实现不需要的接口 |
| D | 依赖倒置 | 依赖抽象而非具体实现 |
// S - 单一职责:拆分不同职责
class User {
constructor(name) { this.name = name; }
}
class UserPersistence {
save(user) { /* 保存到数据库 */ }
load(id) { /* 从数据库读取 */ }
}
常见 class 错误
问题一:忘记 super()
// 错误
class Dog extends Animal {
constructor(name, breed) {
this.breed = breed; // ReferenceError!
super(name); // super 必须在 this 之前
}
}
// 正确
class Dog extends Animal {
constructor(name, breed) {
super(name);
this.breed = breed;
}
}
问题二:class 不会提升
// 错误:class 声明不会像 function 那样提升
let p = new Person("张三"); // ReferenceError!
class Person { /* ... */ }
// 正确:先定义再使用
class Person { /* ... */ }
let p = new Person("张三");
Mixin 混入
不支持多重继承?用 Mixin 实现复用
JS 只支持单继承(一个 extends),但可以通过 Mixin 模式把多个独立功能"混入"到类中,实现灵活的代码复用。
// 定义两个 Mixin
const Serializable = (Base) => class extends Base {
toJSON() {
return JSON.stringify(this, null, 2);
}
static fromJSON(json) {
const data = JSON.parse(json);
return new this(...Object.values(data));
}
};
const Loggable = (Base) => class extends Base {
log(action) {
const time = new Date().toISOString();
console.log(`[${time}] ${this.constructor.name}: ${action}`);
}
};
// 组合使用
class User extends Loggable(Serializable(Object)) {
constructor(name, email) {
super();
this.name = name;
this.email = email;
}
}
let user = new User("张三", "zhang@test.com");
user.log("用户注册"); // [2026-05-14T...] User: 用户注册
console.log(user.toJSON()); // {"name":"张三","email":"zhang@test.com"}
Object.assign(Target.prototype, mixin1, mixin2) 方式混入,但无法在 constructor 中调用混入的 super。
new.target
检测是否被 new 调用的元属性
new.target 是一个元属性,在普通函数调用时为 undefined,在 new 调用时指向实际被调用的构造函数。可以用它实现"抽象类"模式。
// 抽象类:不能直接实例化,必须继承后使用
class Shape {
constructor() {
if (new.target === Shape) {
throw new Error("Shape 是抽象类,不能直接实例化");
}
}
area() {
throw new Error("子类必须实现 area()");
}
describe() {
return `${this.constructor.name} 的面积: ${this.area().toFixed(2)}`;
}
}
class Circle extends Shape {
constructor(radius) {
super(); // 此时 new.target === Circle,不会报错
this.radius = radius;
}
area() { return Math.PI * this.radius ** 2; }
}
// new Shape(); // Error: Shape 是抽象类,不能直接实例化
let c = new Circle(5);
console.log(c.describe()); // "Circle 的面积: 78.54"
new.target 在子类构造函数中通过 super() 调用父类 constructor 时,new.target 仍然指向子类(如 Circle),而非父类(Shape)。这正是实现抽象类模式的关键。
试一试
练习一
任务:创建一个 Stack 类(栈),实现 push、pop、peek、isEmpty 和 size 方法。用 #items 私有字段存储数据。
// 提示:栈是后进先出(LIFO)
// const s = new Stack();
// s.push(1); s.push(2); s.push(3);
// s.pop(); // 3
// s.peek(); // 2
// 你的代码:
参考答案
class Stack {
#items = [];
push(value) { this.#items.push(value); }
pop() { return this.#items.pop(); }
peek() { return this.#items[this.#items.length - 1]; }
isEmpty() { return this.#items.length === 0; }
get size() { return this.#items.length; }
}
练习二
任务:创建 Animal 基类和 Cat、Dog 子类。每个子类重写 speak() 方法,并添加各自特有的方法。
// 提示:Cat 有 purr(),Dog 有 fetch()
// 你的代码:
参考答案
class Animal {
constructor(name) { this.name = name; }
speak() { return `${this.name} makes a sound`; }
}
class Cat extends Animal {
speak() { return `${this.name} meows!`; }
purr() { return `${this.name} is purring`; }
}
class Dog extends Animal {
speak() { return `${this.name} barks!`; }
fetch() { return `${this.name} fetches the ball`; }
}
练习三(挑战)
任务:实现一个 EventEmitter 类,支持 on(event, listener)、off(event, listener)、emit(event, ...args)、once(event, listener) 四个方法。要求用 #listeners 私有字段存储监听器。once 注册的监听器只触发一次后自动移除。
// 提示:#listeners 是一个 Map,key 是事件名,value 是监听器数组
// const emitter = new EventEmitter();
// emitter.on("login", (user) => console.log(user + " 登录了"));
// emitter.emit("login", "张三"); // "张三 登录了"
// 你的代码:
参考答案
class EventEmitter {
#listeners = new Map();
on(event, listener) {
if (!this.#listeners.has(event)) {
this.#listeners.set(event, []);
}
this.#listeners.get(event).push(listener);
return this; // 支持链式调用
}
off(event, listener) {
const list = this.#listeners.get(event);
if (list) {
const idx = list.indexOf(listener);
if (idx !== -1) list.splice(idx, 1);
}
return this;
}
emit(event, ...args) {
const list = this.#listeners.get(event);
if (list) {
// 复制一份,防止遍历中修改原数组
[...list].forEach(fn => fn(...args));
}
return this;
}
once(event, listener) {
const wrapper = (...args) => {
listener(...args);
this.off(event, wrapper);
};
return this.on(event, wrapper);
}
}
// 测试
const emitter = new EventEmitter();
emitter.on("msg", (text) => console.log("收到:", text));
emitter.once("init", () => console.log("仅触发一次"));
emitter.emit("init"); // "仅触发一次"
emitter.emit("init"); // 无输出(已自动移除)
emitter.emit("msg", "你好"); // "收到: 你好"
📝 要点总结
本章核心知识点
class 语法
构造函数、方法、静态成员的声明
继承
extends + super 实现类之间的继承
私有属性
# 前缀实现真正的私有成员
getter/setter
控制属性的读取和赋值逻辑
多态
子类重写父类方法,实现不同行为
下一章
class 让我们更好地组织代码。但当代码量越来越大,文件越来越长时,怎么管理?下一章学习 模块化——把代码拆分成独立、可复用的模块。