通过具体的代码示例演示 JavaScript 中 ES6 类(Class)的定义、实例化、extends 继承以及 super 关键字的实际运行机制,解析面向对象编程的核心要点与常见问题。
JavaScript面向对象实例:类与继承的代码实现演示
时间:2026-10-30
浏览:368
文章目录
class 与 extends 关键字,为基于原型的面向对象编程提供了直观且规范的语法糖。本文通过一套完整的面向对象业务实例,分步演示类的基础定义、构造函数执行、方法派发、子类继承扩展以及 super 关键字的具体运作流程,帮助开发者掌握代码实现细节并规避常见原型调用陷阱。
一、JavaScript 类的基础定义与实例化
在 ECMAScript 2015 之前,开发者通常使用函数作为构造器并在原型对象 prototype 上挂载公共方法。ES6 类将构造逻辑、实例属性和原型方法收拢在统一的代码块内,其核心由类声明、constructor 构造函数以及方法定义三部分构成。
执行 new ClassName(...) 触发实例化时,JavaScript 引擎会在堆内存中分配一个空对象,将其隐式原型 __proto__ 指向类的 prototype,将构造函数内部的 this 绑定到该空对象,并执行构造体内部的代码。如果构造函数没有显式返回其他对象,则自动将该新对象作为实例返回。
// 基础类声明示例
class Device {
// 构造函数:负责实例专属属性的初始化
constructor(model, serialNumber) {
this.model = model;
this.serialNumber = serialNumber;
this.isOnline = false;
}
// 原型方法:所有实例共享的方法
connect(networkName) {
this.isOnline = true;
return `设备 [${this.model}] 已接入网络: ${networkName}`;
}
// 状态获取方法
getStatus() {
const statusText = this.isOnline ? "在线" : "离线";
return `设备编号: ${this.serialNumber} | 当前状态: ${statusText}`;
}
}
// 实例化验证
const router = new Device("RT-AC68U", "SN-981203");
console.log(router.getStatus());
// 示例输出: 设备编号: SN-981203 | 当前状态: 离线
console.log(router.connect("Office-5G"));
// 示例输出: 设备 [RT-AC68U] 已接入网络: Office-5G
console.log(router.getStatus());
// 示例输出: 设备编号: SN-981203 | 当前状态: 在线
二、使用 extends 与 super 实现派生类继承
面向对象设计的核心在于代码复用与职责特化。通过 extends 关键字,子类(派生类)可以完整承接父类的属性与方法;而 super 关键字在子类构造函数中扮演着连接父类构造链的关键角色。
在子类的 constructor 中,必须首先调用 super(...),之后才能使用 this 关键字。这是因为 JavaScript 的继承模型与部分传统面向对象语言不同:在 ES6 类继承中,子类实例对象的构建过程是先由父类构造函数通过 super() 初始化并塑造 this,再由子类对其加工。如果未调用 super() 就访问 this,引擎会直接抛出 ReferenceError。
// 继承父类 Device
class SmartSensor extends Device {
constructor(model, serialNumber, sensorType) {
// 关键步骤:优先调用 super 初始化父类属性
super(model, serialNumber);
// 子类特有属性
this.sensorType = sensorType;
this.batteryLevel = 100;
}
// 扩展子类专属方法
reportData(currentReading) {
if (!this.isOnline) {
return `[${this.sensorType}传感器] 无法上报:设备处于离线状态`;
}
this.batteryLevel = Math.max(0, this.batteryLevel - 1);
return `[${this.sensorType}传感器] 上报成功 | 读数: ${currentReading} | 剩余电量: ${this.batteryLevel}%`;
}
// 方法重写(Override):在复用父类逻辑的基础上追加子类特有信息
getStatus() {
const baseStatus = super.getStatus(); // 复用父类原型方法
return `${baseStatus} | 传感器类型: ${this.sensorType} | 电量: ${this.batteryLevel}%`;
}
}
// 实例化验证
const tempSensor = new SmartSensor("DHT22-Pro", "SN-TEMP-001", "温湿度");
console.log(tempSensor.reportData("24.5°C"));
// 示例输出: [温湿度传感器] 无法上报:设备处于离线状态
// 继承自父类的 connect 方法
console.log(tempSensor.connect("IoT-WLAN"));
// 示例输出: 设备 [DHT22-Pro] 已接入网络: IoT-WLAN
// 子类专属方法
console.log(tempSensor.reportData("24.5°C, 55%RH"));
// 示例输出: [温湿度传感器] 上报成功 | 读数: 24.5°C, 55%RH | 剩余电量: 99%
// 调用重写后的 getStatus 方法
console.log(tempSensor.getStatus());
// 示例输出: 设备编号: SN-TEMP-001 | 当前状态: 在线 | 传感器类型: 温湿度 | 电量: 99%
三、实战演示:电商订单状态机对象模型
为了全面展示属性封装、方法调用、方法覆盖与参数传递的实际组合效果,下面通过一个电商订单模型展开演示。基类 Order 处理通用计费、流水号生成与基础校验,派生类 DiscountOrder 与 ExpressOrder 则分别实现特异化的优惠折扣计算和物流运单追踪。
// 1. 基类:基础订单模型
class Order {
// 私有字段:外部及子类无法直接通过 order.#secretKey 修改
#secretKey;
constructor(orderId, baseAmount) {
if (!Order.isValidAmount(baseAmount)) {
throw new Error(`[订单错误] 基础金额不合法: ${baseAmount}`);
}
this.orderId = orderId;
this.baseAmount = baseAmount;
this.status = "PENDING"; // PENDING, PAID, CANCELLED
this.createdAt = new Date().toISOString().slice(0, 10);
this.#secretKey = `KEY_${Math.random().toString(36).substring(2, 8).toUpperCase()}`;
}
// 静态工具方法:直接通过类名调用
static isValidAmount(amount) {
return typeof amount === "number" && amount > 0;
}
// 计算最终支付金额(基类默认原价)
calculateFinalAmount() {
return Number(this.baseAmount.toFixed(2));
}
// 支付动作
pay() {
if (this.status !== "PENDING") {
return `[订单 ${this.orderId}] 无法支付:当前状态为 ${this.status}`;
}
this.status = "PAID";
return `[订单 ${this.orderId}] 支付完成,实付金额: ¥${this.calculateFinalAmount()}`;
}
// 获取订单详情摘要
getSummary() {
return `单号: ${this.orderId} | 日期: ${this.createdAt} | 状态: ${this.status} | 应付: ¥${this.calculateFinalAmount()}`;
}
}
// 2. 派生类 A:满减/折扣订单
class DiscountOrder extends Order {
constructor(orderId, baseAmount, discountRate) {
// 必须首先调用父类构造函数
super(orderId, baseAmount);
this.discountRate = discountRate; // 例如 0.85 代表八五折
}
// 重写金额计算方法:应用折扣规则
calculateFinalAmount() {
const discounted = this.baseAmount * this.discountRate;
return Number(discounted.toFixed(2));
}
// 扩展子类特有方法
getDiscountInfo() {
const saved = this.baseAmount - this.calculateFinalAmount();
return `[折扣详情] 享受 ${(this.discountRate * 10).toFixed(1)} 折优惠,立减 ¥${saved.toFixed(2)}`;
}
}
// 3. 派生类 B:特快物流配送订单
class ExpressOrder extends Order {
constructor(orderId, baseAmount, expressCompany, shippingFee = 15) {
super(orderId, baseAmount);
this.expressCompany = expressCompany;
this.shippingFee = shippingFee;
this.trackingNumber = null;
}
// 重写金额计算:基础金额 + 快递运费
calculateFinalAmount() {
const total = this.baseAmount + this.shippingFee;
return Number(total.toFixed(2));
}
// 发货方法
dispatch(trackingNumber) {
if (this.status !== "PAID") {
return `[发货失败] 订单 ${this.orderId} 未支付,禁止配货出库`;
}
this.trackingNumber = trackingNumber;
return `[已发货] 承运商: ${this.expressCompany} | 运单号: ${this.trackingNumber}`;
}
// 重写摘要方法:追加物流及运费信息
getSummary() {
const baseSummary = super.getSummary();
const tracking = this.trackingNumber ? this.trackingNumber : "待分配";
return `${baseSummary} (含运费 ¥${this.shippingFee}) | 承运: ${this.expressCompany} | 运单: ${tracking}`;
}
}
// 4. 运行实例检验
const discOrder = new DiscountOrder("ORD-202610-01", 500, 0.8);
console.log(discOrder.getSummary());
// 示例输出: 单号: ORD-202610-01 | 日期: 2026-10-23 | 状态: PENDING | 应付: ¥400
console.log(discOrder.getDiscountInfo());
// 示例输出: [折扣详情] 享受 8.0 折优惠,立减 ¥100.00
console.log(discOrder.pay());
// 示例输出: [订单 ORD-202610-01] 支付完成,实付金额: ¥400
const expressOrder = new ExpressOrder("ORD-202610-02", 120, "顺丰速运", 18);
console.log(expressOrder.pay());
// 示例输出: [订单 ORD-202610-02] 支付完成,实付金额: ¥138
console.log(expressOrder.dispatch("SF9920194820"));
// 示例输出: [已发货] 承运商: 顺丰速运 | 运单号: SF9920194820
console.log(expressOrder.getSummary());
// 示例输出: 单号: ORD-202610-02 | 日期: 2026-10-23 | 状态: PAID | 应付: ¥138 (含运费 ¥18) | 承运: 顺丰速运 | 运单: SF9920194820
四、静态成员、私有属性与访问器
现代 JavaScript 类提供了多层次的数据控制机制:
- 静态成员(
static):直接挂载在类对象本身,而不是挂载在原型或实例上。主要用于定义全局配置、校验函数、类型转换等通用工具方法。子类同样会通过构造函数的原型链继承父类的静态成员。 - 私有属性与方法(
#field):以#开头的字段拥有严格的作用域隔离,只能在该类的声明体内被读取和修改,外部代码及派生子类均无法直接通过属性名访问。这彻底解决了以往依靠下划线约定_privateField进行伪私有化的漏洞。 - 访问器属性(
getter / setter):通过get与set拦截属性的读取与修改行为,常用于字段格式化或赋值约束。
class Account {
// 私有字段:存储敏感余额
#balance = 0;
constructor(owner, initialDeposit = 0) {
this.owner = owner;
if (initialDeposit > 0) {
this.#balance = initialDeposit;
}
}
// Getter 访问器:提供只读保护
get balance() {
return `¥${this.#balance.toFixed(2)}`;
}
// 实例方法:安全修改私有数据
deposit(amount) {
if (amount <= 0) {
throw new Error("存款金额必须大于零");
}
this.#balance += amount;
return `存款成功,当前可用余额: ${this.balance}`;
}
}
const myAccount = new Account("张三", 1000);
console.log(myAccount.balance);
// 示例输出: ¥1000.00
// 尝试直接修改私有字段将触发语法报错
// console.log(myAccount.#balance); // SyntaxError: Private field '#balance' must be declared in an enclosing class
#balance)是不可在子类中被直接继承或读取的。如果子类需要使用父类的私有数据,父类必须通过受保护或公共的 getter/setter、实例方法暴露合法的访问通道。
五、面向对象开发中的常见错误与核心规则
在实际编码与团队协作中,有几个易错点需要特别留意:
- 严格模式默认启用:类声明内部的所有代码块默认都在
"use strict"严格模式下运行,静默赋值失败会直接抛错,未绑定的方法调用中this不会回退至全局对象而是undefined。 - 必须使用 new 调用:与普通函数不同,类不能直接当作普通函数执行(例如调用
Device()),必须使用new Device(),否则引擎会抛出TypeError: Class constructor cannot be invoked without 'new'。 - 构造器内部 this 访问时序:继承类中若包含了
constructor,未书写super()即直接操作this.xxx会触发致命的ReferenceError。 - 没有变量提升(No Hoisting):函数声明会被提升到作用域顶部,但类声明与
let / const一致,存在暂时性死区(TDZ),必须先声明后使用。
实用工具推荐
绿化、压缩、噪点和虚拟水印,自由调节做旧程度。
选择画质和输出格式,预览压缩前后的大小。