搞懂 ES6 的class和继承:从写代码的“土办法”到优雅编程
你有没有过这样的经历?在早期写 JavaScript 时,想做个“人”这个对象,结果只能靠函数模拟:
function Person(name, age) { this.name = name; this.age = age; } Person.prototype.sayHello = function() { console.log(`Hello, I'm ${this.name}`); };看着像那么回事,但总觉得别扭——明明是面向对象的思想,语法却像是拼凑出来的。更头疼的是,一旦要搞个学生、老师这些子类,就得手动操作原型链,一不小心就出错。
直到 ES6 出现了class关键字,一切都变了。
为什么我们需要class?
随着前端项目越来越复杂,单靠对象字面量和函数已经撑不住了。React、Vue 这些框架开始流行组件化开发,我们迫切需要一种更清晰、更有组织性的方式来管理代码逻辑。
ES6 的class就是在这种背景下诞生的。它不是新发明了一套继承机制,而是给老的原型继承穿上了一件漂亮的新外衣——也就是常说的“语法糖”。但它这件衣服穿得太好看了,以至于很多人以为 JavaScript 突然变成了 Java 那样的语言。
其实不然。class只是让原本晦涩难懂的原型操作变得直观易读了。
class到底怎么用?一个例子讲明白
来看一个最简单的例子:
class Person { constructor(name, age) { this.name = name; this.age = age; } sayHello() { console.log(`Hello, I'm ${this.name}`); } static info() { console.log('This is a Person class'); } }这段代码做了三件事:
-构造函数constructor:创建实例时自动执行,用来初始化属性;
-实例方法sayHello():每个由new Person()创建的对象都能调用;
-静态方法info():属于类本身,不归实例所有,直接通过Person.info()调用。
使用起来也很简单:
const p = new Person("Alice", 30); p.sayHello(); // 输出: Hello, I'm Alice Person.info(); // 输出: This is a Person class注意:你不能直接调用Person(),必须用new,否则会报错。这是强制规范,防止误用。
class背后还是原型链,只是你看不见
虽然写法看起来像 Java 或 C++,但底层依然是 JavaScript 原有的那一套——基于原型(prototype-based)的对象系统。
当你写下class Person,JavaScript 实际上做了这些事:
- 把
Person当作一个函数(构造器); - 把所有实例方法(如
sayHello)挂到Person.prototype上; - 静态方法则直接挂在
Person函数上; - 用
new实例化时,执行constructor,并把this绑定到新对象。
所以,下面这行代码依然成立:
p.__proto__ === Person.prototype; // true也就是说,方法查找还是走原型链的老路子。class只是帮你省去了手动写Person.prototype.sayHello = ...的麻烦。
让数据更安全:getter 和 setter
有时候你不希望别人随意修改某些属性,比如银行账户余额。这时候可以用get和set来控制访问。
class BankAccount { constructor(initialBalance = 0) { this._balance = initialBalance; // _ 表示“私有”约定 } get balance() { return this._balance; } set balance(amount) { if (amount < 0) throw new Error("Balance cannot be negative"); this._balance = amount; } deposit(money) { this.balance += money; } withdraw(money) { if (money > this.balance) throw new Error("Insufficient funds"); this._balance -= money; } }现在你可以像读普通属性一样获取余额:
const account = new BankAccount(100); console.log(account.balance); // 100 account.deposit(50); console.log(account.balance); // 150 account.balance = -10; // 抛错!这里的_balance并非真正的私有字段(ES2022 之前没有),只是一个命名约定,提醒开发者:“别在外面直接改我”。
子类怎么来?用extends实现继承
假设我们要做一个学生类,他也有人的基本信息,但多了个年级属性,还能学习。
以前你可能得复制一遍Person的代码再加点东西,但现在只需要一句话:
class Student extends Person { constructor(name, age, grade) { super(name, age); // 必须先调用父类构造函数 this.grade = grade; } study() { console.log(`${this.name} is studying...`); } // 重写父类方法 sayHello() { console.log(`Hi, I'm ${this.name}, a student in grade ${this.grade}`); } }关键点来了:
-extends表示继承自Person;
-super(name, age)是调用父类的constructor,必须在使用this之前调用;
-sayHello方法被重写了,子类优先;
- 新增了study方法,扩展功能。
试试看效果:
const s = new Student("Tom", 20, "Junior"); s.sayHello(); // Hi, I'm Tom, a student in grade Junior s.study(); // Tom is studying...多态也生效了:
function greetEveryone(people) { people.forEach(person => person.sayHello()); } greetEveryone([ new Person("Mr. Lee", 45), new Student("Lucy", 19, "Freshman") ]); // 各自用自己的方式打招呼继承背后的原理:原型链是如何延伸的?
你以为extends是魔法?其实它也只是帮你连好了原型链。
当你说class Student extends Person,JavaScript 在背后悄悄做了几件事:
Student.prototype.__proto__ = Person.prototype
→ 所以实例方法可以向上查找;Student.__proto__ = Person
→ 所以静态方法也能继承;super()实际就是Parent.call(this, ...args)的语法糖;- 方法调用遵循“就近原则”,找不到就往上找。
这也解释了为什么:
s instanceof Student; // true s instanceof Person; // true因为instanceof查的就是原型链。
再进一步:多层继承与方法增强
你可以继续派生更具体的类,比如研究生:
class GraduateStudent extends Student { constructor(name, age, grade, researchTopic) { super(name, age, grade); this.researchTopic = researchTopic; } sayHello() { super.sayHello(); // 先执行父类逻辑 console.log(`Researching on: ${this.researchTopic}`); } static createFromStudent(student, topic) { const { name, age, grade } = student; return new GraduateStudent(name, age, grade, topic); } } const gs = new GraduateStudent("Bob", 25, "PhD", "AI Ethics"); gs.sayHello(); // 输出: // Hi, I'm Bob, a student in grade PhD // Researching on: AI Ethics这里展示了两个高级技巧:
-调用super.sayHello():保留父类行为的同时增加新功能;
-静态工厂方法:提供更灵活的对象创建方式,封装细节。
实际应用场景:什么时候该用类?
别以为class只是用来炫技的。在真实项目中,它有很多实用场景:
✅ UI 组件定义
虽然 React 现在主推函数组件 + Hooks,但在 Vue 2 和一些老项目里,类仍然是组织组件逻辑的重要方式。
class UserCard extends Component { render() { return `<div>Hello ${this.props.name}</div>`; } }✅ 工具类封装
比如封装一个 HTTP 请求客户端:
class HttpClient { constructor(baseURL) { this.baseURL = baseURL; } async get(url) { return fetch(this.baseURL + url).then(r => r.json()); } static createJsonPlaceholder() { return new HttpClient('https://jsonplaceholder.typicode.com'); } }✅ 游戏或管理系统中的实体建模
在一个学生管理系统中,常见的继承结构可能是:
Person ↓ Student ↓ GraduateStudent共用姓名、年龄等基础属性,逐级扩展能力,减少重复代码。
使用类的最佳实践:别踩这些坑
class很强大,但也容易滥用。记住这几个建议:
❌ 不要过度继承
超过两到三层的继承链会让代码难以维护。优先考虑组合代替继承:
// 更好的方式:通过对象组合功能 const CanStudy = { study() { /*...*/ } }; Object.assign(Student.prototype, CanStudy);✅ 合理使用静态方法
静态方法适合做工具函数,不要在里面引用this或实例状态。
✅ 保护内部状态
尽管 ES2022 才正式支持私有字段(#privateField),但你可以用_前缀表示“请勿外部访问”:
this._password = 'secret'; // 约定俗成✅ 方法重写要小心
确保重写后的行为符合预期,尤其是被其他模块依赖的方法,避免破坏 Liskov 替换原则。
✅ 大项目推荐搭配 TypeScript
给类加上类型注解,能极大提升可读性和安全性:
class Person { constructor(public name: string, public age: number) {} }总结一下:你真正需要掌握的是什么?
class是语法糖,本质仍是原型继承;extends和super让继承变得简洁清晰;- 支持构造函数、实例方法、静态方法、getter/setter 完整特性;
- 多态和
instanceof依然有效; - 提高了代码复用性、可读性和团队协作效率。
更重要的是:学会用类去抽象现实世界中的事物关系,而不是为了用而用。
未来,随着私有字段、装饰器等新特性的普及,JavaScript 的类模型还会变得更强大。但无论怎么变,理解其背后的原型机制,才是你驾驭这门语言的根本。
如果你正在写一个复杂的前端应用,不妨试着把核心模型抽象成类。你会发现,代码突然变得有条理了。
如果你在实现过程中遇到了其他挑战,欢迎在评论区分享讨论。