第五章:面向对象编程 (OOP):从语法到思想的结构化之道
第五章:面向对象编程 (OOP):从语法到思想的结构化之道
Prorise第五章:面向对象编程 (OOP):从语法到思想的结构化之道
摘要: 在第四章,我们深入探索了 JavaScript 继承的底层基石——原型链,并一步步推导出了 ES5 时代最优雅的“寄生组合式继承”模式。然而,那个过程是复杂且易错的。本章,我们将学习 ES6 带来的革命性变革:class
关键字。我们将揭示 class
如何作为一层优雅的“语法糖”,将我们之前复杂的原型操作,封装成一种对后端开发者(特别是 Java/Spring Boot 背景)极其友好的、结构化的编程范式。本章的核心并非学习一套全新的体系,而是理解这套新语法是如何让我们更高效、更安全地去运用我们已经掌握的原型继承原理。
在本章,我们将完成一次从底层原理到上层建筑的思维跨越:
- 首先,我们将探讨 OOP 思想为何在大型 JavaScript 项目中至关重要,直面“代码随地乱飞”的开发痛点。
- 接着,我们将逐一学习
class
的核心语法 (constructor
,static
,#private
),并时刻与第四章的原型实现进行对比,理解其“语法糖”的本质。 - 然后,我们将聚焦于
extends
和super
,看看它们是如何将复杂的“寄生组合式继承”和“构造函数窃取”简化为单个关键字的。 - 最后,我们将把 OOP 思想置于 现代前端的生态 中,探讨它在 Angular/NestJS 中的核心地位,以及如何将其分层思想与 React/Vue 的函数式范式进行完美融合。
5.1. OOP 思想在 JavaScript 中的价值
注意: 这一章节适合有面向对象基础的人观看,如果您看不懂,没关系的,在日后的开发中以及现在的前端生态,面向对象的思想在前端体现的会比较少,更多的是组合式,组件化思想
承上启下: 在我们深入 class
语法之前,必须先回答一个根本问题:在一个以函数为一等公民、原型继承如此灵活的语言里,我们为什么还需要看似“刻板”的面向对象编程?
痛点背景:对于习惯了 Java/Spring Boot 这类强结构化框架的开发者而言,初入前端,特别是现代 React/Vue 的函数式组件世界,往往会感到一种“无序感”。数据状态、业务逻辑、API 请求散落在不同的自定义 Hooks 或 Composable 函数中,缺乏一个清晰的、高内聚的组织单元。总会感受到的“代码随地乱飞”。
OOP 思想为解决这一问题提供了强大的武器库:
结构化与封装: 这是 OOP 的核心价值。
class
如同一张蓝图,它强制性地将数据(属性)和操作这些数据的行为(方法)紧密地捆绑在一个独立的、高内聚的单元里。当需要处理“用户”逻辑时,我们去找User
类;需要处理“订单”逻辑,就去找Order
类。这与在 Spring Boot 中寻找UserService
或OrderController
的思路是完全一致的。继承: 允许创建子类来复用和扩展父类的功能,是构建可维护、可扩展代码库的基础。例如,一个
BaseApiController
可以封装通用的错误处理和请求逻辑,所有具体的业务 API 客户端只需继承它即可。抽象与清晰的契约:
class
通过其公共方法向外暴露一个清晰的“契约”,隐藏内部复杂的实现细节。这极大地降低了系统模块间的耦合度,使得大型项目得以协同开发和长期维护。
JavaScript 的 class
语法,正是为了将这些宝贵的 OOP 思想,用一种更简洁、更标准、对开发者更友好的方式引入到语言中。它并没有改变 JavaScript 原型继承的本质,而是为其提供了一个更强大的上层建筑。
5.2. ES6 class
:现代 OOP 的语法基石
现在,让我们来逐一解析 class
的语法,并看看它们分别对应着我们在第四章学到的哪些原型概念。
5.2.1. class
与 constructor
:定义蓝图与实例化
class
提供了一个定义“类”的清晰结构。constructor
方法是类的默认构造函数,通过 new
命令创建对象实例时,会自动调用该方法。
1 | class User { |
与原型的联系 (Aha Moment!):
class User {...}
的声明,本质上等同于我们第四章写的function User(...) {}
构造函数。constructor
方法就是那个构造函数本身。greet()
这个实例方法,class
语法会自动地、正确地将其定义在User.prototype
上,而不是在构造函数里为每个实例重复创建。这正是对第四章“构造函数性能缺陷”的完美优化。
1 | // 上述 class 写法的 ES5 原型等价实现 |
5.2.2. 实例成员与 static
成员
- 实例成员: 定义在
constructor
内部(通过this
)或类体顶层的属性,以及普通的方法。它们属于每一个对象实例。 static
成员: 使用static
关键字修饰的属性和方法。它们不属于任何实例,而是直接属于 类本身。
1 | class User { |
与原型的联系:
static
成员就是直接定义在 构造函数 这个对象上的属性和方法。它等同于:1
2
3function User() {}
User.platform = 'Prorise Blog';
User.getPlatform = function() { /*...*/ };
5.2.3. 私有成员 (#
)
历史痛点: 在原型时代,JavaScript 没有真正的私有成员。开发者只能通过“下划线命名约定”(如 _privateVar
)来“提示”这是一个私有成员,但它在外部依然可以被随意访问和修改,封装性形同虚设。
解决方案: ES2022 引入了 #
前缀,用于定义 真正意义上的私有属性和方法。它们只能在类的内部被访问,外部访问会直接抛出语法错误。
1 | class User { |
1
2
Email: david@example.com
Private field '#email' must be declared in an enclosing class
5.3. 继承 (extends
) 与 super
关键字
这是 class
语法糖最强大的地方,它将我们第四章推导出的复杂继承模式,简化为了两个关键字。
5.3.1. extends
:建立类之间的层级关系
extends
关键字用于让一个类(子类)继承另一个类(父类)。这意味着子类将自动获得父类所有非私有的属性和方法。
基础代码示例:
1 | // 1. 定义一个父类 |
1
2
Hello from the Parent, my name is Prorise.
Prorise
与原型的联系 (The Ultimate Aha Moment!):
class Child extends Parent {}
这一行代码,背后为你做了两件大事,完美对应了“寄生组合式继承”模式:- 它将
Child.prototype
的[[Prototype]]
设置为了Parent.prototype
(等同于Object.create(Parent.prototype)
),从而实现了原型方法的继承。 - 它还将
Child
构造函数自身的[[Prototype]]
设置为了Parent
构造函数,从而实现了静态成员的继承。
- 它将
5.3.2. super()
:在子类构造函数中调用父类
核心规则: 如果子类有 constructor
,则 必须 在其中调用 super()
,并且必须在使用 this
关键字之前调用。
与原型的联系:
super()
在这里作为函数调用,等同于我们之前在子类构造函数中使用的Parent.call(this, ...args)
。它执行父类的构造函数,并将this
绑定到当前子类的实例上,从而实现 实例属性的继承(即“构造函数窃取”)。
5.3.3. super.method()
:在子类中调用父类的方法
当 super
作为对象使用时,它指向父类的原型。这允许我们在子类中调用被覆盖的父类方法,以实现功能的扩展而非完全重写。
综合代码示例 (extends, super(), super.method()):
1 | class Vehicle { |
1
2
Sedan is moving.
It's a Toyota car.
5.4. instanceof
类型检查
5.4.1. instanceof
instanceof
运算符的工作原理没有改变。它依然是通过检查一个对象的 原型链 上是否存在某个构造函数的 prototype
对象来判断。class
语法只是让这个判断的对象来源更清晰。
1 | const myCar = new Car('Sedan', 'Toyota'); |
5.5. OOP 在现代前端生态中的位置与实践
核心讨论: 既然 class
如此强大且结构清晰,为何在现代主流的 React/Vue 框架中,我们看到更多的是函数式组件?
全面拥抱 OOP 的框架: Angular 是最彻底贯彻 OOP 和依赖注入思想的框架。其组件、服务、指令等一切皆为
class
。对于有 Spring Boot 经验的开发者,Angular 的开发模式会感到非常亲切。在后端,NestJS 更是被誉为“Node.js 版的 Spring Boot”,它完全基于 TypeScript 和 OOP,其架构思想(模块、控制器、服务)与 Spring 高度相似,是 Java/C# 开发者进入 Node.js 世界的最佳桥梁。函数式范式的兴起: React Hooks 和 Vue 组合式 API 代表了另一种编程哲学。它们追求 极致的灵活性和逻辑复用,通过将逻辑拆分成可组合的函数单元,而不是封装在类中。
优点: 逻辑复用粒度更细,代码更灵活。
* 缺点: 如果没有良好的架构约束,容易导致逻辑分散、分层不清,即“代码随地乱飞”。
最佳实践:融合两种思想
我们不必陷入“非此即彼”的二元对立。最成熟的工程实践,是 结合两种范式的优点:使用 OOP 的 class
来构建清晰的、分层的业务逻辑(服务层),然后让函数式的 UI 组件(视图层)保持轻薄,只负责消费这些服务并渲染界面。
场景: 我们用 React Hooks 和一个业务逻辑 Class 来重构一个用户列表的加载功能。
1. 创建一个业务逻辑类 (服务层 - The “Service Layer”)
这个文件里没有任何 UI 框架的东西,是纯粹的、可独立测试的业务逻辑。
文件路径: services/UserService.js
1 | // 这个类负责所有与用户数据相关的逻辑 |
2. 在 UI 组件中使用这个 Service (视图层 - The “View Layer”)
这个 React 组件变得非常“薄”,只负责调用 Service 的方法,并根据返回的数据更新视图。
文件路径: components/UserList.jsx
1 | import React, { useState, useEffect } from 'react'; |
这样做的好处:
- 关注点分离 (SoC):
UserService
只关心业务逻辑,UserList
只关心 UI 展示。 - 代码定位清晰: 业务问题去
services
目录找,UI 问题去components
目录找。 - 可测试性:
UserService
可以被独立进行单元测试,无需渲染任何 UI。 - 结构化: 您从 Spring Boot 中欣赏的清晰分层,通过这种方式在前端项目中得到了实现。
5.6. 本章核心原理与高频面试题
核心原理速查
class 语法 | 对应的原型原理 (ES5 等价实现) | 核心价值 |
---|---|---|
constructor(..) | function ClassName(..) | 定义实例属性 |
method() | ClassName.prototype.method = function() {} | 性能优化:在原型上定义共享方法 |
static method() | ClassName.method = function() {} | 定义类级别的方法 |
class Child extends Parent | 寄生组合式继承 | 代码简化:用单个关键字实现最复杂的继承模式 |
super() | Parent.call(this, ...) | 继承父类实例属性 |
super.method() | Parent.prototype.method.call(this) | 调用父类原型上的方法 |
高频面试题与陷阱
请解释一下 JavaScript 的 class
和传统面向对象语言(比如 Java)的 class 有什么本质区别?
最本质的区别在于底层实现。Java 的 class
是一个严格的、编译时的实体,其实例化和继承都基于类。而 JavaScript 的 class
本质上是原型继承的语法糖。它的底层机制仍然是我们第四章讨论的原型链。class
并没有为 JavaScript 引入新的对象继承模型,只是提供了一套更清晰、更结构化的语法来操作已有的原型模型。
很好。那么,在子类的 constructor
中,为什么必须在使用 this
之前调用 super()
?
因为在 ES6 的类继承模型中,子类的实例是通过父类的构造函数来创建的。super()
的作用就是执行父类的构造函数,并让其返回的实例作为子类的 this
。如果在调用 super()
之前就尝试访问 this
,那么这个 this
实际上还不存在,因此 JavaScript 规定这是一种引用错误,以保证继承模型的正确性。