跳到主要内容

JavaScript 对象访问器

什么是对象访问器?

JavaScript对象访问器是一种特殊的方法,用于获取和设置对象的属性值。这些方法被称为"getter"和"setter"。当你想要对属性的读取和写入操作进行更多控制时,访问器特别有用。

  • Getter 方法用于获取属性值
  • Setter 方法用于设置属性值

访问器的强大之处在于它们允许你在访问或修改属性时执行额外的逻辑,如验证、格式化或计算派生值。

为什么需要访问器?

在介绍具体用法前,我们先来理解为什么需要访问器:

  1. 数据验证 - 在设置属性值之前验证数据
  2. 计算属性 - 动态计算属性值
  3. 封装 - 隐藏内部实现细节
  4. 自动触发行为 - 当属性变化时执行特定操作

定义访问器

使用 get 和 set 关键字

在JavaScript中,你可以使用getset关键字定义访问器:

javascript
const person = {
firstName: "John",
lastName: "Doe",

// 定义getter
get fullName() {
return `${this.firstName} ${this.lastName}`;
},

// 定义setter
set fullName(value) {
const parts = value.split(" ");
this.firstName = parts[0];
this.lastName = parts[1];
}
};

// 使用getter
console.log(person.fullName); // 输出: "John Doe"

// 使用setter
person.fullName = "Jane Smith";
console.log(person.firstName); // 输出: "Jane"
console.log(person.lastName); // 输出: "Smith"

这个例子中,fullName看起来像一个普通属性,但实际上它是由get fullName()set fullName()方法定义的访问器属性。

使用 Object.defineProperty()

另一种定义访问器的方法是使用Object.defineProperty()方法:

javascript
const person = {
firstName: "John",
lastName: "Doe"
};

Object.defineProperty(person, "fullName", {
get: function() {
return `${this.firstName} ${this.lastName}`;
},
set: function(value) {
const parts = value.split(" ");
this.firstName = parts[0];
this.lastName = parts[1];
}
});

console.log(person.fullName); // 输出: "John Doe"
person.fullName = "Jane Smith";
console.log(person.firstName); // 输出: "Jane"

只读属性

如果只定义了getter但没有定义setter,那么该属性就变成了只读属性:

javascript
const circle = {
radius: 5,

get area() {
return Math.PI * this.radius * this.radius;
}
};

console.log(circle.area); // 输出: 约78.54

// 尝试设置area属性
circle.area = 100; // 这不会改变area的值,在严格模式下会报错
console.log(circle.area); // 仍然输出: 约78.54
警告

在严格模式下,尝试设置只有getter没有setter的属性会抛出错误。

数据验证示例

访问器的一个常见用途是在设置属性值之前进行数据验证:

javascript
const user = {
_email: "",

get email() {
return this._email;
},

set email(value) {
// 简单的电子邮件验证
if (!/^\S+@\S+\.\S+$/.test(value)) {
throw new Error("无效的电子邮件格式");
}
this._email = value;
}
};

try {
user.email = "example@domain.com";
console.log(user.email); // 输出: "example@domain.com"

user.email = "invalid-email"; // 将抛出错误
} catch (e) {
console.log(e.message); // 输出: "无效的电子邮件格式"
}
备注

注意上面例子中使用的属性名前的下划线_。这是一种常见的命名约定,表示该属性是"私有"的,不应直接访问。

在类中使用访问器

在ES6类中,访问器的定义更为简洁:

javascript
class Person {
constructor(firstName, lastName) {
this._firstName = firstName;
this._lastName = lastName;
}

get firstName() {
return this._firstName;
}

set firstName(value) {
if (typeof value !== 'string') {
throw new Error('firstName必须是字符串');
}
this._firstName = value;
}

get lastName() {
return this._lastName;
}

set lastName(value) {
if (typeof value !== 'string') {
throw new Error('lastName必须是字符串');
}
this._lastName = value;
}

get fullName() {
return `${this._firstName} ${this._lastName}`;
}
}

const person = new Person("John", "Doe");
console.log(person.fullName); // 输出: "John Doe"

person.firstName = "Jane";
console.log(person.fullName); // 输出: "Jane Doe"

try {
person.firstName = 123; // 将抛出错误
} catch (e) {
console.log(e.message); // 输出: "firstName必须是字符串"
}

实际应用场景

1. 表单验证

javascript
class Form {
constructor() {
this._username = '';
this._password = '';
}

get username() {
return this._username;
}

set username(value) {
if (value.length < 3) {
throw new Error('用户名至少需要3个字符');
}
this._username = value;
}

get password() {
return '********'; // 不返回实际密码
}

set password(value) {
if (value.length < 8) {
throw new Error('密码至少需要8个字符');
}
if (!/[A-Z]/.test(value)) {
throw new Error('密码需要至少一个大写字母');
}
this._password = value;
}
}

const loginForm = new Form();

try {
loginForm.username = "ab"; // 将抛出错误
} catch (e) {
console.log(e.message); // 输出: "用户名至少需要3个字符"
}

try {
loginForm.username = "user";
console.log("用户名有效:", loginForm.username); // 输出: "用户名有效: user"

loginForm.password = "weak"; // 将抛出错误
} catch (e) {
console.log(e.message); // 输出: "密码至少需要8个字符"
}

2. 计算属性 - 购物车示例

javascript
class ShoppingCartItem {
constructor(name, price, quantity) {
this.name = name;
this.price = price;
this._quantity = quantity;
}

get quantity() {
return this._quantity;
}

set quantity(value) {
if (value < 0) {
throw new Error('数量不能为负数');
}
this._quantity = value;
}

get subtotal() {
return this.price * this._quantity;
}
}

class ShoppingCart {
constructor() {
this.items = [];
}

addItem(item) {
this.items.push(item);
}

get total() {
return this.items.reduce((sum, item) => sum + item.subtotal, 0);
}
}

const cart = new ShoppingCart();
cart.addItem(new ShoppingCartItem("笔记本电脑", 5000, 1));
cart.addItem(new ShoppingCartItem("耳机", 200, 2));

console.log(`购物车总金额: ¥${cart.total}`); // 输出: "购物车总金额: ¥5400"

// 修改数量
cart.items[1].quantity = 3;
console.log(`更新后的总金额: ¥${cart.total}`); // 输出: "更新后的总金额: ¥5600"

3. 温度转换器

javascript
class TemperatureConverter {
constructor(celsius = 0) {
this._celsius = celsius;
}

get celsius() {
return this._celsius;
}

set celsius(value) {
this._celsius = value;
}

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

set fahrenheit(value) {
this._celsius = (value - 32) * 5/9;
}

get kelvin() {
return this._celsius + 273.15;
}

set kelvin(value) {
this._celsius = value - 273.15;
}
}

const temp = new TemperatureConverter(25);
console.log(`${temp.celsius}°C = ${temp.fahrenheit}°F = ${temp.kelvin}K`);
// 输出: "25°C = 77°F = 298.15K"

temp.fahrenheit = 68;
console.log(`${temp.celsius}°C = ${temp.fahrenheit}°F = ${temp.kelvin}K`);
// 输出约: "20°C = 68°F = 293.15K"

对象访问器的优点

  1. 数据封装 - 可以隐藏内部实现细节,只暴露必要的接口
  2. 数据验证 - 可以在设置属性值之前验证数据
  3. 计算属性 - 可以动态计算属性值,而不需要存储
  4. 向后兼容 - 可以在不破坏现有代码的情况下修改属性的实现
  5. 监听变化 - 可以在属性更改时触发特定操作

总结

JavaScript对象访问器提供了一种强大的机制来控制对对象属性的访问和修改。通过使用getter和setter方法,你可以:

  • 在访问属性时动态计算值
  • 在设置属性值之前进行验证
  • 在属性更改时触发其他操作
  • 隐藏内部实现细节,提高代码的封装性

这些特性使得访问器成为构建健壮且可维护的JavaScript应用程序的重要工具。

练习

  1. 创建一个Rectangle类,它有widthheight属性,并使用getter实现一个只读的area属性。
  2. 扩展上面的Rectangle类,为widthheight添加setter,确保它们不能设为负值。
  3. 创建一个BankAccount类,使用访问器来确保balance属性不能直接被修改,而是通过depositwithdraw方法改变。
  4. 实现一个Person类,包含firstNamelastName属性,并提供一个fullName访问器,当fullName被设置时,能够正确地更新firstNamelastName
进阶阅读

想深入学习JavaScript对象访问器,可以查阅以下资源: