访问器(Getters 和 Setters)
概述
访问器(Accessors)是 TypeScript 类中的一个重要特性,使用 get 和 set 关键字可以创建属性的访问器方法。Getter 用于读取属性值,Setter 用于设置属性值。访问器允许我们在访问或修改属性时执行额外的逻辑,如数据验证、计算属性、副作用处理等。这使得类的属性访问更加灵活和安全。
基本语法
Getter(读取器)
使用 get 关键字定义属性的读取器,当访问该属性时会自动调用 getter 方法:
typescript
class Circle {
private _radius: number;
constructor(radius: number) {
this._radius = radius;
}
// Getter:读取半径
get radius(): number {
return this._radius;
}
// Getter:计算面积
get area(): number {
return Math.PI * this._radius * this._radius;
}
// Getter:计算周长
get circumference(): number {
return 2 * Math.PI * this._radius;
}
}
const circle = new Circle(5);
console.log(circle.radius); // 5(访问 getter)
console.log(circle.area); // 78.53981633974483(计算属性)
console.log(circle.circumference); // 31.41592653589793(计算属性)Setter(设置器)
使用 set 关键字定义属性的设置器,当给该属性赋值时会自动调用 setter 方法:
typescript
class Temperature {
private _celsius: number;
constructor(celsius: number) {
this._celsius = celsius;
}
// Getter:获取摄氏度
get celsius(): number {
return this._celsius;
}
// Setter:设置摄氏度(带验证)
set celsius(value: number) {
if (value < -273.15) {
throw new Error("Temperature cannot be below absolute zero");
}
this._celsius = value;
}
// Getter:获取华氏度(计算属性)
get fahrenheit(): number {
return (this._celsius * 9) / 5 + 32;
}
// Setter:设置华氏度(自动转换为摄氏度)
set fahrenheit(value: number) {
this._celsius = ((value - 32) * 5) / 9;
}
}
const temp = new Temperature(25);
console.log(temp.celsius); // 25
console.log(temp.fahrenheit); // 77
temp.celsius = 30; // 使用 setter
console.log(temp.celsius); // 30
console.log(temp.fahrenheit); // 86
temp.fahrenheit = 100; // 使用 setter(自动转换)
console.log(temp.celsius); // 37.77777777777778提示
访问器使用起来就像普通属性一样,但实际上是方法调用。Getter 和 Setter 可以分别定义,也可以只定义其中一个。
基本用法
只读属性(只有 Getter)
如果只定义 getter 而不定义 setter,该属性就变成了只读属性:
typescript
class User {
private _id: number;
private _name: string;
private _email: string;
constructor(id: number, name: string, email: string) {
this._id = id;
this._name = name;
this._email = email;
}
// 只读属性:ID 不能修改
get id(): number {
return this._id;
}
// 可读写属性:姓名
get name(): string {
return this._name;
}
set name(value: string) {
if (value.trim().length === 0) {
throw new Error("Name cannot be empty");
}
this._name = value;
}
// 可读写属性:邮箱(带验证)
get email(): string {
return this._email;
}
set email(value: string) {
if (!value.includes("@")) {
throw new Error("Invalid email address");
}
this._email = value;
}
}
const user = new User(1, "Alice", "alice@example.com");
console.log(user.id); // 1
console.log(user.name); // "Alice"
user.name = "Bob"; // ✅ 可以修改
user.email = "bob@example.com"; // ✅ 可以修改
// ❌ 错误:ID 是只读的
// user.id = 2; // Error: Cannot set property id of #<User> which has only a getter数据验证
Setter 非常适合用于数据验证,确保属性值符合要求:
typescript
class BankAccount {
private _balance: number = 0;
private _accountNumber: string;
constructor(accountNumber: string) {
this._accountNumber = accountNumber;
}
get balance(): number {
return this._balance;
}
// Setter 中不能直接设置余额,需要通过方法控制
// 但可以用于验证其他属性
get accountNumber(): string {
return this._accountNumber;
}
set accountNumber(value: string) {
if (!/^[A-Z]{2}\d{8}$/.test(value)) {
throw new Error("Account number must be in format: XX12345678");
}
this._accountNumber = value;
}
public deposit(amount: number): void {
if (amount <= 0) {
throw new Error("Deposit amount must be positive");
}
this._balance += amount;
}
public withdraw(amount: number): boolean {
if (amount <= 0) {
throw new Error("Withdrawal amount must be positive");
}
if (amount > this._balance) {
return false;
}
this._balance -= amount;
return true;
}
}
const account = new BankAccount("AB12345678");
account.deposit(1000);
console.log(account.balance); // 1000
// ✅ 正确:符合格式的账户号
account.accountNumber = "CD87654321";
// ❌ 错误:不符合格式
// account.accountNumber = "invalid"; // Error: Account number must be in format: XX12345678计算属性
Getter 可以用于创建计算属性,这些属性的值是根据其他属性动态计算的:
typescript
class Rectangle {
private _width: number;
private _height: number;
constructor(width: number, height: number) {
this._width = width;
this._height = height;
}
get width(): number {
return this._width;
}
set width(value: number) {
if (value <= 0) {
throw new Error("Width must be positive");
}
this._width = value;
}
get height(): number {
return this._height;
}
set height(value: number) {
if (value <= 0) {
throw new Error("Height must be positive");
}
this._height = value;
}
// 计算属性:面积
get area(): number {
return this._width * this._height;
}
// 计算属性:周长
get perimeter(): number {
return 2 * (this._width + this._height);
}
// 计算属性:是否为正方形
get isSquare(): boolean {
return this._width === this._height;
}
// 计算属性:对角线长度
get diagonal(): number {
return Math.sqrt(this._width * this._width + this._height * this._height);
}
}
const rect = new Rectangle(10, 5);
console.log(rect.area); // 50(自动计算)
console.log(rect.perimeter); // 30(自动计算)
console.log(rect.isSquare); // false(自动计算)
console.log(rect.diagonal); // 11.180339887498949(自动计算)
rect.width = 8;
console.log(rect.area); // 40(自动重新计算)高级用法
访问器与访问修饰符
访问器可以与访问修饰符(public、private、protected)组合使用:
typescript
class Product {
private _price: number;
private _discount: number = 0;
constructor(price: number) {
this._price = price;
}
// 公共 getter
public get price(): number {
return this._price;
}
// 公共 setter(带验证)
public set price(value: number) {
if (value < 0) {
throw new Error("Price cannot be negative");
}
this._price = value;
}
// 私有 getter(只能在类内部使用)
private get discount(): number {
return this._discount;
}
// 公共方法访问私有 getter
public getDiscountedPrice(): number {
return this.price * (1 - this.discount);
}
public setDiscount(percentage: number): void {
if (percentage < 0 || percentage > 1) {
throw new Error("Discount must be between 0 and 1");
}
this._discount = percentage;
}
}
const product = new Product(100);
console.log(product.price); // 100
console.log(product.getDiscountedPrice()); // 100
product.setDiscount(0.2); // 20% 折扣
console.log(product.getDiscountedPrice()); // 80
// ❌ 错误:不能直接访问私有 getter
// console.log(product.discount); // Error: Property 'discount' is private访问器与只读属性
如果只有 getter 而没有 setter,属性就是只读的,这与 readonly 关键字类似:
typescript
class Person {
private _id: number;
private _name: string;
private _birthDate: Date;
constructor(id: number, name: string, birthDate: Date) {
this._id = id;
this._name = name;
this._birthDate = birthDate;
}
// 只读属性:ID
get id(): number {
return this._id;
}
// 可读写属性:姓名
get name(): string {
return this._name;
}
set name(value: string) {
this._name = value;
}
// 只读属性:年龄(计算属性)
get age(): number {
const today = new Date();
let age = today.getFullYear() - this._birthDate.getFullYear();
const monthDiff = today.getMonth() - this._birthDate.getMonth();
if (monthDiff < 0 || (monthDiff === 0 && today.getDate() < this._birthDate.getDate())) {
age--;
}
return age;
}
// 只读属性:出生日期
get birthDate(): Date {
return new Date(this._birthDate); // 返回副本,防止外部修改
}
}
const person = new Person(1, "Alice", new Date(1990, 0, 1));
console.log(person.id); // 1
console.log(person.age); // 自动计算的年龄
console.log(person.birthDate); // 出生日期
person.name = "Bob"; // ✅ 可以修改
// ❌ 错误:只读属性不能修改
// person.id = 2; // Error
// person.age = 30; // Error(只有 getter)
// person.birthDate = new Date(); // Error(只有 getter)延迟计算(Lazy Evaluation)
Getter 可以用于实现延迟计算,只在第一次访问时才计算值:
typescript
class ExpensiveComputation {
private _result: number | null = null;
private _input: number;
constructor(input: number) {
this._input = input;
}
// 延迟计算:只在第一次访问时计算
get result(): number {
if (this._result === null) {
console.log("Computing expensive result...");
// 模拟耗时计算
this._result = this._input * this._input * this._input;
}
return this._result;
}
// 重置计算结果
public reset(): void {
this._result = null;
}
}
const computation = new ExpensiveComputation(10);
// 此时还没有计算
console.log(computation.result); // "Computing expensive result..." 然后输出 1000
console.log(computation.result); // 直接输出 1000(不再计算)
computation.reset();
console.log(computation.result); // "Computing expensive result..." 然后输出 1000使用示例
示例 1:用户配置管理
typescript
class UserSettings {
private _theme: "light" | "dark" = "light";
private _language: string = "en";
private _notifications: boolean = true;
private _fontSize: number = 14;
// 主题设置(带验证)
get theme(): "light" | "dark" {
return this._theme;
}
set theme(value: "light" | "dark") {
if (value !== "light" && value !== "dark") {
throw new Error("Theme must be 'light' or 'dark'");
}
this._theme = value;
this._saveSettings(); // 自动保存
}
// 语言设置
get language(): string {
return this._language;
}
set language(value: string) {
if (value.length !== 2) {
throw new Error("Language code must be 2 characters");
}
this._language = value.toLowerCase();
this._saveSettings();
}
// 通知设置
get notifications(): boolean {
return this._notifications;
}
set notifications(value: boolean) {
this._notifications = value;
this._saveSettings();
}
// 字体大小(带范围验证)
get fontSize(): number {
return this._fontSize;
}
set fontSize(value: number) {
if (value < 8 || value > 72) {
throw new Error("Font size must be between 8 and 72");
}
this._fontSize = value;
this._saveSettings();
}
// 私有方法:保存设置
private _saveSettings(): void {
console.log("Settings saved:", {
theme: this._theme,
language: this._language,
notifications: this._notifications,
fontSize: this._fontSize
});
// 实际应用中会保存到 localStorage 或服务器
}
// 计算属性:设置摘要
get summary(): string {
return `Theme: ${this._theme}, Language: ${this._language}, Font: ${this._fontSize}px`;
}
}
const settings = new UserSettings();
console.log(settings.theme); // "light"
settings.theme = "dark"; // "Settings saved: ..."
settings.fontSize = 16; // "Settings saved: ..."
console.log(settings.summary); // "Theme: dark, Language: en, Font: 16px"
// ❌ 错误:无效的主题
// settings.theme = "blue"; // Error: Theme must be 'light' or 'dark'示例 2:购物车系统
typescript
class CartItem {
private _productId: number;
private _productName: string;
private _price: number;
private _quantity: number;
constructor(productId: number, productName: string, price: number, quantity: number = 1) {
this._productId = productId;
this._productName = productName;
this._price = price;
this._quantity = quantity;
}
// 只读属性:产品 ID
get productId(): number {
return this._productId;
}
// 只读属性:产品名称
get productName(): string {
return this._productName;
}
// 只读属性:单价
get price(): number {
return this._price;
}
// 可读写属性:数量(带验证)
get quantity(): number {
return this._quantity;
}
set quantity(value: number) {
if (value < 1) {
throw new Error("Quantity must be at least 1");
}
if (!Number.isInteger(value)) {
throw new Error("Quantity must be an integer");
}
this._quantity = value;
}
// 计算属性:小计
get subtotal(): number {
return this._price * this._quantity;
}
}
class ShoppingCart {
private _items: CartItem[] = [];
// 只读属性:购物车项目列表
get items(): readonly CartItem[] {
return [...this._items]; // 返回副本,防止外部修改
}
// 计算属性:总价
get total(): number {
return this._items.reduce((sum, item) => sum + item.subtotal, 0);
}
// 计算属性:商品总数
get itemCount(): number {
return this._items.reduce((sum, item) => sum + item.quantity, 0);
}
// 计算属性:是否为空
get isEmpty(): boolean {
return this._items.length === 0;
}
public addItem(item: CartItem): void {
const existingItem = this._items.find(i => i.productId === item.productId);
if (existingItem) {
existingItem.quantity += item.quantity;
} else {
this._items.push(item);
}
}
public removeItem(productId: number): boolean {
const index = this._items.findIndex(item => item.productId === productId);
if (index > -1) {
this._items.splice(index, 1);
return true;
}
return false;
}
public clear(): void {
this._items = [];
}
}
const cart = new ShoppingCart();
const item1 = new CartItem(1, "Laptop", 999, 1);
const item2 = new CartItem(2, "Mouse", 29, 2);
cart.addItem(item1);
cart.addItem(item2);
console.log(cart.total); // 1057
console.log(cart.itemCount); // 3
console.log(cart.isEmpty); // false
item2.quantity = 3; // 使用 setter
console.log(cart.total); // 1086示例 3:表单验证
typescript
class FormField {
private _value: string = "";
private _errors: string[] = [];
private _isDirty: boolean = false;
constructor(private _name: string, private _validator?: (value: string) => string[]) {
}
// 值(带验证)
get value(): string {
return this._value;
}
set value(newValue: string) {
this._value = newValue;
this._isDirty = true;
this._validate();
}
// 只读属性:字段名
get name(): string {
return this._name;
}
// 只读属性:错误列表
get errors(): readonly string[] {
return [...this._errors];
}
// 计算属性:是否有错误
get hasErrors(): boolean {
return this._errors.length > 0;
}
// 计算属性:是否有效
get isValid(): boolean {
return !this.hasErrors && this._isDirty;
}
// 计算属性:是否已修改
get isDirty(): boolean {
return this._isDirty;
}
// 私有方法:验证
private _validate(): void {
this._errors = [];
if (this._validator) {
const validationErrors = this._validator(this._value);
this._errors.push(...validationErrors);
}
}
public reset(): void {
this._value = "";
this._errors = [];
this._isDirty = false;
}
}
// 邮箱验证器
function emailValidator(value: string): string[] {
const errors: string[] = [];
if (!value) {
errors.push("Email is required");
} else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)) {
errors.push("Invalid email format");
}
return errors;
}
// 密码验证器
function passwordValidator(value: string): string[] {
const errors: string[] = [];
if (!value) {
errors.push("Password is required");
} else if (value.length < 8) {
errors.push("Password must be at least 8 characters");
} else if (!/[A-Z]/.test(value)) {
errors.push("Password must contain at least one uppercase letter");
} else if (!/[a-z]/.test(value)) {
errors.push("Password must contain at least one lowercase letter");
} else if (!/\d/.test(value)) {
errors.push("Password must contain at least one number");
}
return errors;
}
const emailField = new FormField("email", emailValidator);
const passwordField = new FormField("password", passwordValidator);
emailField.value = "invalid-email";
console.log(emailField.hasErrors); // true
console.log(emailField.errors); // ["Invalid email format"]
emailField.value = "user@example.com";
console.log(emailField.hasErrors); // false
console.log(emailField.isValid); // true
passwordField.value = "weak";
console.log(passwordField.errors); // ["Password must be at least 8 characters", ...]
passwordField.value = "StrongPass123";
console.log(passwordField.hasErrors); // false
console.log(passwordField.isValid); // true类型检查示例
常见错误
typescript
// ❌ 错误:Getter 不能有参数
class Example1 {
// get value(param: string): string { // Error: A 'get' accessor cannot have parameters
// return param;
// }
}
// ❌ 错误:Setter 必须有且仅有一个参数
class Example2 {
private _value: string;
// set value(): void { // Error: A 'set' accessor must have exactly one parameter
// this._value = "";
// }
// set value(v1: string, v2: string): void { // Error: A 'set' accessor must have exactly one parameter
// this._value = v1 + v2;
// }
}
// ❌ 错误:访问器不能有返回类型注解(getter 除外)
class Example3 {
private _value: string;
get value(): string { // ✅ Getter 可以有返回类型
return this._value;
}
// set value(v: string): void { // ❌ Setter 不能有返回类型注解
// this._value = v;
// }
}正确写法
typescript
// ✅ 正确:Getter 和 Setter 的基本用法
class User {
private _name: string;
get name(): string {
return this._name;
}
set name(value: string) {
this._name = value;
}
}
const user = new User();
user.name = "Alice";
console.log(user.name); // "Alice"
// ✅ 正确:只有 Getter(只读属性)
class Config {
private _apiKey: string = "default";
get apiKey(): string {
return this._apiKey;
}
}
const config = new Config();
console.log(config.apiKey); // "default"
// config.apiKey = "new"; // Error: Cannot set property apiKey
// ✅ 正确:Getter 和 Setter 的类型可以不同(不推荐)
class Converter {
private _value: number = 0;
get value(): number {
return this._value;
}
set value(v: string | number) {
this._value = typeof v === "string" ? parseFloat(v) : v;
}
}
const converter = new Converter();
converter.value = "123";
console.log(converter.value); // 123 (number)注意事项
提示
- 访问器使用起来像普通属性,但实际上是方法调用
- Getter 和 Setter 可以分别定义,也可以只定义其中一个
- 如果只有 getter,属性就是只读的
- Getter 非常适合用于计算属性和延迟计算
- Setter 非常适合用于数据验证和副作用处理
- 访问器可以与访问修饰符(public、private、protected)组合使用
注意
- Getter 不能有参数,Setter 必须有且仅有一个参数
- Setter 不能有返回类型注解(void 也不可以)
- 访问器在编译后会转换为普通方法,不会影响运行时性能
- 如果同时定义了同名的普通属性和访问器,访问器会覆盖普通属性
- 访问器中的
this指向类的实例
重要
- 访问器是 TypeScript/JavaScript 的特性,编译后的代码中会保留
- 不要在 getter 中执行耗时操作,因为每次访问属性都会调用 getter
- 对于需要频繁访问的计算属性,考虑使用缓存机制
- Setter 中应该进行数据验证,确保数据的一致性
- 访问器不能用于解构赋值:
const { value } = obj;不会调用 getter
信息
访问器的优势:
- 封装性:隐藏内部实现,提供统一的访问接口
- 验证性:在设置值时进行数据验证
- 计算性:动态计算属性值
- 灵活性:可以在访问或修改时执行额外逻辑
适用场景:
- 需要数据验证的属性
- 计算属性(基于其他属性计算)
- 只读属性(只有 getter)
- 需要副作用的属性访问(如自动保存、日志记录)
- 延迟计算(Lazy Evaluation)
相关链接
- 类基础 - 了解类的基本概念
- 访问修饰符 - 学习访问修饰符的使用
- 只读属性 - 了解只读属性的用法
- 私有字段 - 学习私有字段的使用
- TypeScript 官方文档 - 类