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 项目。
评论已关闭