TypeScript 中如何使用 getter 和 setter

TypeScript 中如何使用 getter 和 setter

一、背景与问题

在面向对象编程中,getter 和 setter 是封装属性访问的核心手段。它们允许开发者在读取或写入属性时执行额外的逻辑,例如数据验证、触发副作用、缓存计算结果等。TypeScript 作为静态类型语言,提供了对 getter 和 setter 的强大支持,但其底层机制和实际应用场景常被开发者忽视。

本篇文章将深入探讨 TypeScript 中 getter 和 setter 的实现原理、使用场景、常见陷阱以及性能优化策略,帮助开发者在实际项目中做出更合理的封装决策。


二、基本原理

1. 语法结构

TypeScript 中的 getter 和 setter 是通过 get 和 set 关键字定义的,它们与类中的属性绑定:

class Example {
  private _value: number;

  get value(): number {
    return this._value;
  }

  set value(value: number) {
    this._value = value;
  }
}
  • get 定义了属性的读取逻辑。
  • set 定义了属性的写入逻辑。
  • 属性名(value)在 get/set 中保持一致。

2. 与私有字段的结合

TypeScript 2.8+ 引入了 private 字段修饰符,使得 getter/setter 与私有字段的配合更加紧密:

class User {
  private _name: string;

  constructor(name: string) {
    this._name = name;
  }

  get name(): string {
    return this._name;
  }

  set name(name: string) {
    if (name.trim().length < 3) {
      throw new Error('Name must be at least 3 characters');
    }
    this._name = name;
  }
}

此时,_name 是实际存储数据的字段,而 name 是对外暴露的接口。

3. 与继承的关系

getter/setter 会随着类的继承被继承,但默认不会被重写(除非显式使用 override 关键字)。这种行为在设计类层次结构时需要特别注意。


三、环境准备

  • TypeScript 4.8+
  • Node.js 18+
  • IDE:VS Code(支持类型检查和代码提示)

四、核心实现

1. 基础用法

class Temperature {
  private _celsius: number;

  constructor(celsius: number) {
    this._celsius = celsius;
  }

  get celsius(): number {
    return this._celsius;
  }

  set celsius(celsius: number) {
    this._celsius = celsius;
  }

  get fahrenheit(): number {
    return (this._celsius * 9 / 5) + 32;
  }
}

关键点分析:

  • fahrenheit 是一个计算属性,无需显式存储。
  • celsius 是可读可写属性,通过 setter 控制输入值。

2. 数据验证与副作用

class BankAccount {
  private _balance: number = 0;

  get balance(): number {
    return this._balance;
  }

  set balance(value: number) {
    if (value < 0) {
      throw new Error('Balance cannot be negative');
    }
    this._balance = value;
  }

  deposit(amount: number): void {
    this.balance += amount;
    console.log(`Deposited ${amount}, new balance: ${this.balance}`);
  }
}

关键点分析:

  • balance 的 setter 实现了数据验证。
  • deposit 方法通过 balance 属性间接修改值,确保所有修改都经过验证。

3. 缓存与性能优化

class Cache {
  private _data: Map<string, number> = new Map();

  get data(): Map<string, number> {
    return this._data;
  }

  set data(data: Map<string, number>) {
    this._data = data;
  }

  getCache(key: string): number | undefined {
    return this._data.get(key);
  }

  setCache(key: string, value: number): void {
    this._data.set(key, value);
  }
}

关键点分析:

  • data 是缓存接口,通过 getter/setter 控制访问。
  • 外部代码无需直接操作 Map 实例,避免了内存泄漏风险。

五、完整案例

1. 用户管理系统的完整实现

class User {
  private _id: number;
  private _name: string;
  private _email: string;
  private _passwordHash: string;

  constructor(id: number, name: string, email: string, password: string) {
    this._id = id;
    this._name = name;
    this._email = email;
    this._passwordHash = this.hashPassword(password);
  }

  get id(): number {
    return this._id;
  }

  get name(): string {
    return this._name;
  }

  set name(name: string) {
    if (name.trim().length < 3) {
      throw new Error('Name must be at least 3 characters');
    }
    this._name = name;
  }

  get email(): string {
    return this._email;
  }

  set email(email: string) {
    if (!this.validateEmail(email)) {
      throw new Error('Invalid email format');
    }
    this._email = email;
  }

  get passwordHash(): string {
    return this._passwordHash;
  }

  private hashPassword(password: string): string {
    return btoa(password); // 简化示例
  }

  private validateEmail(email: string): boolean {
    const re = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/;
    return re.test(email);
  }
}

关键点分析:

  • id 是只读属性,通过 getter 提供访问。
  • name 和 email 的 setter 包含业务逻辑验证。
  • passwordHash 是只读属性,防止直接修改敏感数据。

使用示例:

const user = new User(1, 'Alice', 'alice@example.com', '123456');
console.log(user.name); // Alice
user.name = 'Bob'; // 合法
user.name = 'A'; // 抛出错误

六、源码解析

1. 编译后的 JavaScript

TypeScript 编译器会将 getter/setter 转换为 JavaScript 的 get/set 方法:

class User {
  constructor(id, name, email, password) {
    this._id = id;
    this._name = name;
    this._email = email;
    this._passwordHash = this.hashPassword(password);
  }

  get id() {
    return this._id;
  }

  get name() {
    return this._name;
  }

  set name(name) {
    if (name.trim().length < 3) {
      throw new Error('Name must be at least 3 characters');
    }
    this._name = name;
  }

  // 其他方法...
}

2. 与私有字段的绑定

TypeScript 的 private 字段在编译后会转换为 _propertyName 形式,getter/setter 通过 this.propertyName 访问,确保封装性。


七、进阶使用

1. 与装饰器结合

function LogProperty(target: any, propertyName: string) {
  const value = target[propertyName];
  Object.defineProperty(target, propertyName, {
    get: () => {
      console.log(`Getting ${propertyName}`);
      return value;
    },
    set: (newValue) => {
      console.log(`Setting ${propertyName}`);
      value = newValue;
    },
    enumerable: true
  });
}

使用示例:

class Config {
  @LogProperty
  private _timeout: number = 1000;
}

2. 与代理模式结合

class DataProxy {
  private _data: Record<string, any> = {};

  get data(): Record<string, any> {
    return this._data;
  }

  set data(data: Record<string, any>) {
    this._data = data;
  }

  get(key: string): any {
    return this._data[key];
  }

  set(key: string, value: any): void {
    this._data[key] = value;
  }
}

八、性能与工程实践

1. 性能优化策略

场景优化方案
频繁访问计算属性使用缓存字段,如 private _cache: Map<string, any>
高频写入属性使用 set 方法进行批量处理
大数据对象使用 WeakMap 或 Proxy 管理动态属性

2. 异常处理

class Validator {
  private _data: Record<string, any> = {};

  get data(): Record<string, any> {
    return this._data;
  }

  set data(data: Record<string, any>) {
    if (!this.validate(data)) {
      throw new Error('Invalid data');
    }
    this._data = data;
  }

  private validate(data: Record<string, any>): boolean {
    return Object.keys(data).every(key => {
      if (typeof data[key] === 'string') {
        return data[key].length > 0;
      }
      return true;
    });
  }
}

3. 安全风险

  • 数据泄露:直接暴露私有字段(如 this._passwordHash)可能导致敏感信息泄露。
  • 注入攻击:未正确验证输入值可能导致 SQL 注入或 XSS 攻击。
  • 缓存污染:未正确管理缓存可能导致数据不一致。

九、常见问题与踩坑

1. 常见错误

错误原因解决方案
Property 'name' has no initializer and is not declared in the constructor忘记初始化私有字段在构造函数中显式初始化
Type 'string' is not assignable to type 'number'类型不匹配在 set 方法中进行类型校验
Cannot assign to 'name' because it is a getter错误地尝试直接修改属性使用 set 方法控制修改

2. 高级陷阱

  • getter/setter 与 Object.defineProperty 的差异:JavaScript 原生的 Object.defineProperty 无法直接支持类型检查,而 TypeScript 的 getter/setter 会强制类型校验。
  • 继承中的 getter/setter 重写:子类需要显式使用 override 关键字,否则会创建新的属性。

十、最佳实践

1. 使用建议

场景推荐方案
需要数据验证使用 set 方法校验输入
需要计算属性使用 get 方法定义计算逻辑
需要缓存使用私有字段存储计算结果
需要安全控制在 set 方法中进行输入过滤

2. 避免滥用

场景不建议使用原因
简单属性使用直接属性避免过度封装
频繁访问属性使用 get 方法增加性能开销
大数据对象使用 get 方法可能导致内存泄漏

3. 工程实践

  • 统一封装策略:对所有业务属性使用 getter/setter,保持代码一致性。
  • 类型安全:利用 TypeScript 的类型系统,避免运行时错误。
  • 文档注释:为 getter/setter 添加详细注释,说明其用途和限制。

十一、总结

TypeScript 的 getter 和 setter 是封装属性访问的强大工具,但其使用需要结合具体场景。通过合理设计 getter/setter,可以实现数据验证、计算属性、缓存控制等高级功能。然而,过度使用会导致代码复杂化,而滥用则可能引入安全风险和性能问题。

在实际项目中,建议:

  • 在需要数据校验、计算属性或副作用的场景使用 getter/setter;
  • 避免对简单属性进行封装;
  • 结合 TypeScript 的类型系统,确保代码安全;
  • 通过单元测试验证 getter/setter 的行为。

通过深入理解 getter 和 setter 的原理和最佳实践,开发者可以更高效地构建安全、可维护的 TypeScript 项目。

评论已关闭

推荐阅读

AIGC实战——Transformer模型
2024年12月01日
Socket TCP 和 UDP 编程基础(Python)
2024年11月30日
python , tcp , udp
如何使用 ChatGPT 进行学术润色?你需要这些指令
2024年12月01日
AI
最新 Python 调用 OpenAi 详细教程实现问答、图像合成、图像理解、语音合成、语音识别(详细教程)
2024年11月24日
ChatGPT 和 DALL·E 2 配合生成故事绘本
2024年12月01日
omegaconf,一个超强的 Python 库!
2024年11月24日
【视觉AIGC识别】误差特征、人脸伪造检测、其他类型假图检测
2024年12月01日
[超级详细]如何在深度学习训练模型过程中使用 GPU 加速
2024年11月29日
Python 物理引擎pymunk最完整教程
2024年11月27日
MediaPipe 人体姿态与手指关键点检测教程
2024年11月27日
深入了解 Taipy:Python 打造 Web 应用的全面教程
2024年11月26日
基于Transformer的时间序列预测模型
2024年11月25日
Python在金融大数据分析中的AI应用(股价分析、量化交易)实战
2024年11月25日
AIGC Gradio系列学习教程之Components
2024年12月01日
Python3 `asyncio` — 异步 I/O,事件循环和并发工具
2024年11月30日
llama-factory SFT系列教程:大模型在自定义数据集 LoRA 训练与部署
2024年12月01日
Python 多线程和多进程用法
2024年11月24日
Python socket详解,全网最全教程
2024年11月27日
python之plot()和subplot()画图
2024年11月26日
理解 DALL·E 2、Stable Diffusion 和 Midjourney 工作原理
2024年12月01日