第3章 3.3 类与继承(ES6 Class)
上章回顾:上一章我们聊了对象和原型链,知道 JavaScript 里一切皆对象,每个对象都偷偷拿着另一个对象的引用——原型。就像每个中国人心里都装着一个「炎黄」的引用,代代相传。原型链就是靠着这种「继承炎黄血脉」的方式,让对象能用到祖先定义的方法。
但问题来了——原型链写法太麻烦了!每次都要写 Person.prototype.sayHi = function(){},而且继承要写好几行 prototype 操作。有没有更直观的方式?
这一章,我们学一个让 JavaScript「面向对象」更优雅的语法——Class。
🎯 开场 3 分钟:为什么要学这个?
场景引入
想象你开了一家奶茶店。
- 某天你研发了一款「珍珠奶茶」,配方是:茶底 + 奶 + 珍珠
- 生意好了,你要推出「椰果奶茶」「芋泥奶茶」……每款都要在「珍珠奶茶」基础上改一点点
- 如果每款都要从头写配方,你会疯掉的
类(Class)就是配方模板。你定义一次,多个对象共享。
\n\n
\n\n
\n\n 痛点
你一定遇到过这种代码:
function Person(name, age) {
this.name = name;
this.age = age;
}
Person.prototype.greet = function() {
console.log('你好,我是' + this.name);
};
为什么要写 prototype?新人看了直接懵。这章让你甩掉这个包袱。
承诺
学完本章,你能:
- 用 Class 语法定义「配方模板」
- 用 extends 实现继承(子类自动拥有父类的配方)
- 写出可读性翻倍的面向对象代码
🧱 基础 25 分钟:核心概念
3.3.1 Class 是个啥?
类就是「把一群相关数据和操作打包成一个整体」的语法。
生活类比:类就像菜谱。
- 菜谱里写清楚:需要什么食材(属性)、每步怎么做(方法)
- 照着菜谱可以做出无数道菜(实例)
定义一个最简单的类
class Person {
// 构造器:创建对象时自动调用的「初始化方法」
constructor(name, age) {
this.name = name; // 你的名字
this.age = age; // 你的年龄
}
// 这是一个方法:类的「行为」
greet() {
console.log('你好,我是' + this.name);
}
}
// 用 new 关键字「照着菜谱做菜」——创建实例
let xiaoming = new Person('小明', 18);
xiaoming.greet(); // 输出:你好,我是小明
这行在干嘛:
- class Person{} 定义了一个「人类配方」
- constructor 是初始化方法,创建对象时自动执行
- new Person(...) 是「用配方创造一个具体的人」
3.3.2 继承 extends——子承父业
继承就是子类自动拥有父类的一切。
生活类比:继承就像儿子继承父亲的银行卡——不用申请,天然就有。
// 父类:人类
class Person {
constructor(name, age) {
this.name = name;
this.age = age;
}
greet() {
console.log('你好,我是' + this.name);
}
}
// 子类:学生(继承自 Person)
class Student extends Person {
constructor(name, age, score) {
// super() 必须在 this 之前调用!
super(name, age); // 调用父类的构造器
this.score = score; // 子类自己新增的属性
}
// 子类新增的方法
study() {
console.log(this.name + '正在学习,分数是' + this.score);
}
}
let xiaohong = new Student('小红', 16, 95);
xiaohong.greet(); // 继承自父类,输出:你好,我是小红
xiaohong.study(); // 子类自己的方法,输出:小红正在学习,分数是95
这行在干嘛:
- extends 关键字声明「Student 继承 Person」
- super(name, age) 调用父类的构造器,完成父类部分的初始化
- 子类不用重写 greet(),直接用父亲留下的
3.3.3 static——属于类本身,不属于实例
生活类比:static 就像「菜谱总册」本身的属性——比如「本菜谱共收录 100 道菜」。这是菜谱本身的信息,不是某道具体菜的信息。
class MathTool {
// static 方法:直接用类名调用,不用 new
static add(a, b) {
return a + b;
}
// static 属性:属于类本身
static version = '1.0';
}
console.log(MathTool.add(3, 5)); // 8
console.log(MathTool.version); // 1.0
// 错误用法:let t = new MathTool(); t.add(1,2) // 报错!
这行在干嘛:
- static 方法直接通过 类名.方法名() 调用
- 不需要 new 创建实例,节省内存
3.3.4 getter / setter——属性的看门人
生活类比:getter/setter 就像银行柜员——你不能直接动金库,但你可以通过柜员(getter 读、setter 改)来操作,而且柜员可以加验证逻辑。
class User {
constructor(name) {
this._name = name; // 约定:私有属性用下划线开头
}
// getter:读取 name 时自动触发
get name() {
console.log('有人读取了 name');
return this._name;
}
// setter:写入 name 时自动触发
set name(value) {
if (value.length < 2) {
console.log('名字太短了!');
return;
}
console.log('有人修改了 name 为' + value);
this._name = value;
}
}
let user = new User('张三');
console.log(user.name); // 输出:有人读取了 name + 返回张三
user.name = '李四'; // 输出:有人修改了 name 为李四
user.name = 'a'; // 输出:名字太短了!(验证生效)
这行在干嘛:
- get name() 让你用 对象.name 读取属性时执行自定义逻辑
- set name(value) 让你写入时执行验证
- 外部代码调用方式和普通属性一模一样
3.3.5 private 字段(ES2022 新语法)——真正的隐私
生活类比:private 就像家里的保险柜密码——只有家里人知道,外面的人根本看不到。
class BankAccount {
// #开头的是私有字段,外部无法访问
#balance = 0;
deposit(amount) {
this.#balance += amount;
}
getBalance() {
return this.#balance;
}
}
let account = new BankAccount();
account.deposit(1000);
console.log(account.getBalance()); // 1000
// console.log(account.#balance); // 报错!私有字段无法外部访问
这行在干嘛:
- #balance 是真正的私有字段,外部代码无法读写
- 只有类内部的方法可以访问
- 比下划线约定更安全(那是「防君子不防小人」)
🔥 实战 35 分钟:3 个递进的小项目
项目 1(5 分钟):学生信息卡
跟着抄,理解 Class 基本用法。
需求:创建 3 个学生,输出他们的自我介绍。
class Student {
constructor(name, age, className) {
this.name = name;
this.age = age;
this.className = className;
}
introduce() {
console.log('我是' + this.name + ',' + this.age + '岁,在' + this.className + '班');
}
}
// 创建3个学生实例
let students = [
new Student('小明', 15, '初三(1)'),
new Student('小红', 14, '初二(3)'),
new Student('小强', 16, '高一(2)')
];
// 让每个学生自我介绍
students.forEach(s => s.introduce());
预期输出:
我是小明,15岁,在初三(1)班
我是小红,14岁,在初二(3)班
我是小强,16岁,在高一(2)班
一句话解释:用 Class 模板创建了 3 个学生对象,每个都有相同的结构(name/age/className)和行为(introduce)。
项目 2(15 分钟):电商订单系统
从 JSON 数据读取订单,用 Class 封装处理逻辑。
需求:有一批订单数据,计算每个用户的总消费,并找出消费最高的用户。
// 模拟从 API 获取的订单数据
let orders = [
{ orderId: 'A001', user: '张三', amount: 299, date: '2024-01-15' },
{ orderId: 'A002', user: '李四', amount: 1299, date: '2024-01-15' },
{ orderId: 'A003', user: '张三', amount: 599, date: '2024-01-16' },
{ orderId: 'A004', user: '王五', amount: 89, date: '2024-01-16' },
{ orderId: 'A005', user: '李四', amount: 2399, date: '2024-01-17' },
];
class OrderAnalyzer {
constructor(orders) {
this.orders = orders;
}
// 计算每个用户的总消费
getUserTotalSpending() {
let spending = {};
for (let order of this.orders) {
if (!spending[order.user]) {
spending[order.user] = 0;
}
spending[order.user] += order.amount;
}
return spending;
}
// 找出消费最高的用户
getTopSpender() {
let spending = this.getUserTotalSpending();
let topUser = null;
let maxAmount = 0;
for (let user in spending) {
if (spending[user] > maxAmount) {
maxAmount = spending[user];
topUser = user;
}
}
return { user: topUser, amount: maxAmount };
}
}
// 使用分析器
let analyzer = new OrderAnalyzer(orders);
console.log('各用户消费:', analyzer.getUserTotalSpending());
console.log('消费冠军:', analyzer.getTopSpender());
预期输出:
各用户消费: { '张三': 898, '李四': 3698, '王五': 89 }
消费冠军: { user: '李四', amount: 3698 }
一句话解释:把订单数据封装进 OrderAnalyzer 类,用「面向对象」的方式组织数据和处理逻辑,代码更清晰。
项目 3(15 分钟):任务待办清单工具
组合前两个项目的能力,做一个有点真实用途的小工具。
需求:实现一个待办清单,支持添加任务、标记完成、统计完成率。
class Task {
constructor(title, priority = '中') {
this.title = title;
this.priority = priority;
this.completed = false;
this.createdAt = new Date().toLocaleDateString();
}
complete() {
this.completed = true;
}
getInfo() {
let status = this.completed ? '[✓]' : '[ ]';
return `${status} ${this.title} (优先级:${this.priority} 创建于:${this.createdAt})`;
}
}
class TodoList {
constructor(name) {
this.name = name;
this.tasks = [];
}
addTask(title, priority) {
let task = new Task(title, priority);
this.tasks.push(task);
console.log('已添加:' + title);
}
completeTask(title) {
let task = this.tasks.find(t => t.title === title);
if (task) {
task.complete();
console.log('已完成:' + title);
} else {
console.log('没找到任务:' + title);
}
}
showAll() {
console.log('\n===== ' + this.name + ' =====');
this.tasks.forEach(t => console.log(t.getInfo()));
console.log('完成率:' + this.getCompletionRate());
}
getCompletionRate() {
if (this.tasks.length === 0) return '0%';
let completed = this.tasks.filter(t => t.completed).length;
return Math.round(completed / this.tasks.length * 100) + '%';
}
}
// 模拟使用
let myTodo = new TodoList('小明的工作清单');
// 添加任务
myTodo.addTask('写日报', '高');
myTodo.addTask('回复邮件', '中');
myTodo.addTask('整理桌面', '低');
myTodo.addTask('开会', '高');
// 标记完成
myTodo.completeTask('写日报');
myTodo.completeTask('回复邮件');
// 展示清单
myTodo.showAll();
预期输出:
已添加:写日报
已添加:回复邮件
已添加:整理桌面
已添加:开会
已完成:写日报
已完成:回复邮件
===== 小明的工作清单 =====
[✓] 写日报 (优先级:高 创建于:2024/1/15)
[✓] 回复邮件 (优先级:中 创建于:2024/1/15)
[ ] 整理桌面 (优先级:低 创建于:2024/1/15)
[ ] 开会 (优先级:高 创建于:2024/1/15)
完成率:50%
一句话解释:用 Task 类代表单个任务,用 TodoList 类管理一群任务,展示了类的组合用法。
💪 进阶 20 分钟:常见坑 + 性能小贴士
坑 1:忘记调用 super()
// ❌ 错误写法
class Student extends Person {
constructor(name, age, score) {
this.score = score; // 报错!先调用 super
}
}
// ✅ 正确写法
class Student extends Person {
constructor(name, age, score) {
super(name, age); // super 必须在最前面
this.score = score;
}
}
原因:子类必须先完成父类的初始化,才能用 this。
坑 2:把方法写成箭头函数
// ❌ 错误写法
class Person {
constructor(name) {
this.name = name;
}
greet = () => { // 箭头函数没有 prototype,无法被继承链正确访问
console.log('你好');
}
}
// ✅ 正确写法
class Person {
constructor(name) {
this.name = name;
}
greet() { // 普通方法
console.log('你好,我是' + this.name);
}
}
坑 3:static 方法里用了 this
// ❌ 错误写法
class MathTool {
static add(a, b) {
return this.multiply(a, b); // static 方法里 this 指向类本身,不是实例
}
static multiply(a, b) {
return a * b;
}
}
// ✅ 正确写法
class MathTool {
static add(a, b) {
return MathTool.multiply(a, b); // 用类名调用
}
static multiply(a, b) {
return a * b;
}
}
坑 4:private 字段名字写错
class BankAccount {
#balance = 0;
getBalance() {
// ❌ 错误:这里访问的是普通属性,不是私有字段
return this.balance; // undefined
}
}
// ✅ 正确:访问私有字段要用 # 前缀
class BankAccount {
#balance = 0;
getBalance() {
return this.#balance; // 正确
}
}
坑 5:getter/setter 里又触发自己(死循环)
// ❌ 错误写法
class User {
constructor(name) {
this.name = name; // 这里会触发 setter!
}
get name() {
return this._name;
}
set name(value) {
this.name = value; // 死循环!又触发 setter
}
}
// ✅ 正确写法:用不同的内部存储
class User {
constructor(name) {
this._name = name; // 用 _name 存储,不触发 setter
}
get name() {
return this._name;
}
set name(value) {
this._name = value;
}
}
调试技巧:console.log 打印对象结构
class Student {
constructor(name, age) {
this.name = name;
this.age = age;
}
}
let s = new Student('小明', 15);
console.log('对象结构:', s);
console.log('是 Student 的实例吗?', s instanceof Student);
输出:
对象结构: Student { name: '小明', age: 15 }
是 Student 的实例吗? true
✏️ 练习题
练习 1(2 分钟):继承体验
- 输入:创建 Animal 类有 eat() 方法,Dog 类继承它
- 预期输出:调用 dog.eat() 输出「吃东西」
- 提示:子类构造器里别忘了 super()
练习 2(2 分钟):static 练手
- 输入:写一个 Counter 类,有 static count = 0,每 new 一次 count++
- 预期输出:创建 3 个实例后,Counter.count 等于 3
- 提示:static 属性属于类本身,不属于实例
练习 3(2 分钟):getter 验证
- 输入:写一个 Temperature 类,摄氏度和华氏度互相转换
- 预期输出:t.celsius = 100; console.log(t.fahrenheit) 输出 212
- 提示:用 getter/setter 实现自动转换
练习 4(3 分钟):待办清单升级
- 输入:在项目 3 基础上,给 TodoList 加一个 removeTask(title) 方法
- 预期输出:删除后该任务不再出现在列表中
- 提示:用 filter 或 splice
练习 5(3 分钟):报错分析
- 输入:下面代码会报错吗?为什么?
class A {
constructor() {
this.x = 1;
}
}
class B extends A {
constructor() {
this.y = 2; // 这里会报错!
super();
}
}
- 预期输出:报错(ReferenceError)
- 提示:
super()必须在this之前
作业:做一个「个人理财小助手」
- 需求描述:帮小明记录日常收支,计算每月结余
- 功能点:
1.Account类:账户名、余额,有deposit()存钱、withdraw()取钱方法
2.Transaction类:记录每笔交易(金额、类型、日期、备注)
3.FinanceManager类:管理多个账户和所有交易记录,能统计某月的收支情况 - 加分项:
1. 用 private 字段保护余额不被直接修改
2. 支持多个账户间转账 - 验收标准:能跑起来 + 存钱取钱正确 + 统计功能输出符合预期
- 提交方式:评论区贴代码或 GitHub 链接
📚 总结 + 资源
本章 3 句总结
- Class 是 JS 面向对象的语法糖,比 prototype 写起来更直观
- extends 实现继承,子类通过
super()调用父类构造器 - static / private / getter/setter 是 Class 的高级装备,让代码更安全
延伸资源
- MDN Class 文档——最权威的参考资料
- 《JavaScript 高级程序设计》第 4 章——系统学习面向对象
- JavaScript.info Class 章节——图文并茂的进阶教程
互动钩子:你在项目里用过 Class 吗?是用它封装组件、还是写工具类?评论区聊聊你是怎么用的,老粉优先回复!
下章预告:学会了 Class 和继承,下一章我们要接触 JS 里另一类「容器」——Set、Map、WeakMap。它们和普通数组/对象有什么区别?什么时候该用它们?卖个关子,下章见!

评论(0)