第3章 3.3 类与继承(ES6 Class)

上章回顾:上一章我们聊了对象和原型链,知道 JavaScript 里一切皆对象,每个对象都偷偷拿着另一个对象的引用——原型。就像每个中国人心里都装着一个「炎黄」的引用,代代相传。原型链就是靠着这种「继承炎黄血脉」的方式,让对象能用到祖先定义的方法。

但问题来了——原型链写法太麻烦了!每次都要写 Person.prototype.sayHi = function(){},而且继承要写好几行 prototype 操作。有没有更直观的方式?

这一章,我们学一个让 JavaScript「面向对象」更优雅的语法——Class


🎯 开场 3 分钟:为什么要学这个?

场景引入

想象你开了一家奶茶店。

  • 某天你研发了一款「珍珠奶茶」,配方是:茶底 + 奶 + 珍珠
  • 生意好了,你要推出「椰果奶茶」「芋泥奶茶」……每款都要在「珍珠奶茶」基础上改一点点
  • 如果每款都要从头写配方,你会疯掉的

类(Class)就是配方模板。你定义一次,多个对象共享。

\n\nSimple tech illustration expla\n\nAI comic creation scene, creat\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) 方法
- 预期输出:删除后该任务不再出现在列表中
- 提示:用 filtersplice

练习 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 的高级装备,让代码更安全

延伸资源

  1. MDN Class 文档——最权威的参考资料
  2. 《JavaScript 高级程序设计》第 4 章——系统学习面向对象
  3. JavaScript.info Class 章节——图文并茂的进阶教程

互动钩子:你在项目里用过 Class 吗?是用它封装组件、还是写工具类?评论区聊聊你是怎么用的,老粉优先回复!


下章预告:学会了 Class 和继承,下一章我们要接触 JS 里另一类「容器」——Set、Map、WeakMap。它们和普通数组/对象有什么区别?什么时候该用它们?卖个关子,下章见!

声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理。