JS 面向对象编程:如何优雅地处理类中的 this 指向与继承陷阱

2025-12-19

在 JavaScript 中,“Classes の Classes” 通常指的是类的嵌套(Nested Classes)或者类的继承(Inheritance)。虽然 JavaScript 不像 Java 那样经常使用显式的“内部类”,但在处理复杂逻辑时,这依然是一个非常强大的工具。

让我们用轻松的方式来看看在使用类时经常遇到的坑,以及更聪明的替代方案吧!

这是开发者最容易抓狂的地方。当你把类的方法作为回调函数(比如在 setTimeout 或事件监听器中)传递时,this 往往会丢失,指向 undefined 或全局对象。

容易出错的代码

class Timer {
  constructor() {
    this.seconds = 0;
  }
  start() {
    setInterval(function() {
      this.seconds++; // 报错!这里的 this 不是 Timer 实例
      console.log(this.seconds);
    }, 1000);
  }
}

解决方法(箭头函数)
箭头函数不会创建自己的 this,它会捕获外层上下文的 this

class Timer {
  constructor() {
    this.seconds = 0;
  }
  start() {
    setInterval(() => {
      this.seconds++; 
      console.log(`计时: ${this.seconds}s`);
    }, 1000);
  }
}

有时候我们会想在一个类里面再定义一个类(Classes inside Classes),以此来实现封装。

潜在问题
内部类如果过度依赖外部类的私有属性,会导致代码非常难以测试和维护。

更好的替代方案
组合(Composition) 与其嵌套类,不如将逻辑拆分成独立的类,然后通过构造函数注入。这样代码更模块化!

代码示例

// 独立的引擎类
class Engine {
  start() { console.log("引擎启动..."); }
}

// 汽车类组合了引擎类
class Car {
  constructor() {
    this.engine = new Engine(); // 组合
  }
  drive() {
    this.engine.start();
    console.log("汽车出发了!");
  }
}

如果你在子类中写了 constructor,你必须先调用 super(),否则 JavaScript 会毫不留情地抛出错误,因为它还没初始化父类的 this

正确的继承方式

class Animal {
  constructor(name) {
    this.name = name;
  }
}

class Dog extends Animal {
  constructor(name, breed) {
    super(name); // 必须先调用 super!
    this.breed = breed;
  }
  bark() {
    console.log(`${this.name} (${this.breed}) 在汪汪叫!`);
  }
}

虽然类(Classes)很工整,但现代 JavaScript(特别是 React 等框架)越来越倾向于函数(Functions)。

类的问题
可能会让代码包(Bundle Size)变大,逻辑复用较难。

函数的优势
更加简洁,易于压缩优化,逻辑复用可以通过自定义 Hook 或高阶函数实现。

简单的对比


需要处理 this,状态分散在各个生命周期。

闭包/函数
状态逻辑更集中,更符合现代开发直觉。

小心 this
尽量用箭头函数。

多用“组合”,少用“深度继承”
让类保持独立和简单。

别忘了 super()
它是子类构造函数的敲门砖。


javascript



Temporal API 实践:如何准确获取和操作 PlainDateTime 对象的“秒”

请注意由于 Temporal API 相对较新,并且在撰写本文时它仍然处于 Stage 3 提案阶段,所以你在实际使用时可能需要一个 Polyfill 或者目标环境需要支持它。Temporal. PlainDateTime. second 属性用于获取 PlainDateTime 对象所表示的日期时间中的秒字段,它的值是一个介于 0 到 59 之间的整数。


再见 String.strike():用 和 CSS 替代 JavaScript 中已弃用的文本划线方法

String. prototype. strike() 方法是一个 不推荐使用 (deprecated) 的字符串方法。它的作用是创建一个 <strike> HTML 元素,将原始字符串包裹起来,表示该文本已被删除或作废。返回值 包含在 <strike> 标签中的字符串。


Map.has() 使用疑难杂症解析:为什么不能用 get() == undefined 来判断键是否存在?

请注意,我会使用“助手”来指代我,以避免使用您提到的词语。Map. has(key) 方法是用来检查 Map 对象中是否包含指定的 键(key) 的一个非常基础且常用的方法。返回值 如果 Map 中存在该键,则返回 true;否则返回 false。



如何从 Temporal 日期时间中获取 PlainTime?故障排除与替代方案

这个方法是 JavaScript Temporal API 的一部分,用于从一个日期时间对象中提取时间部分。Temporal. PlainDateTime表示不带时区信息的日期和时间(例如2024-12-25T143000)。toPlainTime()作用就是从这个 PlainDateTime 中只取出时间部分,返回一个新的 Temporal


国际化 (Intl) API 深度解析:如何准确获取和处理 Locale 区域代码

我将用友好的简体中文,详细解释它的常见问题、替代方法,并提供代码示例,帮助您更好地理解和应用它。Intl. Locale. prototype. region 属性返回 区域(Region) 或 国家(Country) 代码,通常是一个 ISO 3166-1 alpha-2 代码,例如 'US'(美国)、'CN'(中国)、'JP'(日本)等。


从安全到性能:深入解析 eval() 的三大陷阱及安全高效的替代方法

eval() 是 JavaScript 全局对象 (Global Object) 上的一个函数,它的主要作用是将一个字符串作为 JavaScript 代码来执行。它接收一个字符串参数,并尝试将这个字符串解析和执行为 JavaScript 代码。如果字符串表示一个表达式,eval() 会返回这个表达式的值。如果字符串表示一个或多个语句,eval() 会执行这些语句并返回最后一个表达式的值,如果没有表达式,则返回 undefined。


代码单元与字符?Intl.Segmenter.containing() 索引定位详解与实践

这个功能是用来获取给定字符串中特定索引位置的完整文本段(Segment)信息的。它在处理需要准确分割语言文本(比如按字符、词、句子等)的场景中非常有用。请注意:在你的提问中,你写的是 Intl. segmenter. segment. Segments


JavaScript 精确求和新星:Math.sumPrecise 提案详解

Math. sumPrecise 是一个用于 精确计算一组数字总和 的提案,旨在解决标准 IEEE 754 浮点数 运算中常见的精度丢失问题。目前它是一个处于 Stage 3 阶段 的提案,意味着规范基本确定,但可能还没有被所有主流浏览器完全实现(但某些浏览器如 Firefox 的新版本可能已支持)。


JavaScript 开发者必读:Temporal.PlainYearMonth 避坑指南与实战案例

目前的 Date 对象在处理日期和时间时确实让人有些头疼(比如月份从 0 开始算、夏令时问题等)。为了解决这些痛点,全新的 Temporal API 应运而生。今天我们就专门聊聊 Temporal. PlainYearMonth。它非常适合处理像“2025年12月”这种没有具体天数的日期场景,比如信用卡过期时间或某个月的课表。


告别时区困扰:用 Temporal.Instant.until() 精准计算时长

Temporal. Instant. prototype. until() 的作用是计算当前 Instant 到另一个 Instant 之间的时间差,并返回一个 Temporal. Duration 对象。Temporal. Instant 代表时间线上的一个唯一且精确的点,不包含时区或日历信息。until() 方法计算的是两个绝对时间点之间的纳秒差。