getter/setter 与属性描述符:Object.defineProperty 的现代写法
理解属性描述符
JavaScript 中的对象属性不仅有值,还有一组描述符,用于控制属性的行为。这些描述符包括 configurable、enumerable、writable 以及 get/set 方法。
属性描述符的基本概念
javascript
// 使用 Object.defineProperty 定义属性
const obj = {};
Object.defineProperty(obj, 'name', {
value: 'Alice',
writable: true, // 可写
enumerable: true, // 可枚举
configurable: true // 可配置
});
console.log(obj.name); // Alice
// 检查属性描述符
console.log(Object.getOwnPropertyDescriptor(obj, 'name'));
// { value: 'Alice', writable: true, enumerable: true, configurable: true }数据属性 vs 访问器属性
javascript
const obj = {};
// 数据属性
Object.defineProperty(obj, 'dataProperty', {
value: 'data value',
writable: true,
enumerable: true,
configurable: true
});
// 访问器属性
Object.defineProperty(obj, 'accessorProperty', {
get: function() {
return this._value || 'default';
},
set: function(value) {
console.log(`Setting value to: ${value}`);
this._value = value;
},
enumerable: true,
configurable: true
});
console.log(obj.dataProperty); // data value
console.log(obj.accessorProperty); // default
obj.accessorProperty = 'new value'; // Setting value to: new value
console.log(obj.accessorProperty); // new valuegetter 和 setter 的现代写法
ES6 Class 中的 getter 和 setter
javascript
class Person {
constructor(firstName, lastName) {
this.firstName = firstName;
this.lastName = lastName;
this._age = 0;
}
// getter
get fullName() {
return `${this.firstName} ${this.lastName}`;
}
// setter
set fullName(value) {
const parts = value.split(' ');
this.firstName = parts[0];
this.lastName = parts[1];
}
// 带验证的 setter
get age() {
return this._age;
}
set age(value) {
if (value < 0 || value > 150) {
throw new Error('Invalid age');
}
this._age = value;
}
}
const person = new Person('John', 'Doe');
console.log(person.fullName); // John Doe
person.fullName = 'Jane Smith';
console.log(person.firstName); // Jane
console.log(person.lastName); // Smith
person.age = 30;
console.log(person.age); // 30
// person.age = -5; // Error: Invalid age对象字面量中的 getter 和 setter
javascript
const user = {
_name: 'Alice',
_email: '',
// getter
get name() {
return this._name;
},
// setter
set name(value) {
if (typeof value !== 'string' || value.length === 0) {
throw new Error('Name must be a non-empty string');
}
this._name = value;
},
get email() {
return this._email;
},
set email(value) {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!emailRegex.test(value)) {
throw new Error('Invalid email format');
}
this._email = value;
}
};
console.log(user.name); // Alice
user.name = 'Bob';
console.log(user.name); // Bob
user.email = 'bob@example.com';
console.log(user.email); // bob@example.comObject.defineProperty 的详细用法
定义多个属性
javascript
const obj = {};
// 定义多个属性
Object.defineProperties(obj, {
name: {
value: 'Product',
writable: true,
enumerable: true,
configurable: true
},
price: {
value: 100,
writable: false, // 只读属性
enumerable: true,
configurable: false
},
category: {
value: 'Electronics',
writable: true,
enumerable: false, // 不可枚举
configurable: true
}
});
console.log(obj.name); // Product
console.log(obj.price); // 100
obj.price = 200; // 无效,因为 writable: false
console.log(obj.price); // 100
// 不可枚举属性不会出现在 for...in 循环中
for (let key in obj) {
console.log(key); // name (不包括 category)
}
console.log(Object.keys(obj)); // ['name'] (不包括 category)创建只读属性
javascript
const config = {};
Object.defineProperty(config, 'API_URL', {
value: 'https://api.example.com',
writable: false, // 不可写
enumerable: true, // 可枚举
configurable: false // 不可配置
});
console.log(config.API_URL); // https://api.example.com
config.API_URL = 'new url'; // 无效
console.log(config.API_URL); // https://api.example.com
// 尝试重新定义会失败
// Object.defineProperty(config, 'API_URL', { value: 'new url' }); // TypeError创建不可枚举属性
javascript
const obj = {
visible: 'I am visible'
};
Object.defineProperty(obj, 'hidden', {
value: 'I am hidden',
writable: true,
enumerable: false, // 不可枚举
configurable: true
});
console.log(obj.visible); // I am visible
console.log(obj.hidden); // I am hidden
// 不可枚举属性不会出现在以下操作中
for (let key in obj) {
console.log(key); // 只输出 visible
}
console.log(Object.keys(obj)); // ['visible']
console.log(JSON.stringify(obj)); // {"visible":"I am visible"}
// 但仍可以通过 getOwnPropertyNames 获取
console.log(Object.getOwnPropertyNames(obj)); // ['visible', 'hidden']getter/setter 的高级应用
计算属性
javascript
class Rectangle {
constructor(width, height) {
this.width = width;
this.height = height;
}
// 计算属性
get area() {
console.log('Calculating area...');
return this.width * this.height;
}
get perimeter() {
console.log('Calculating perimeter...');
return 2 * (this.width + this.height);
}
// 动态属性
get diagonal() {
return Math.sqrt(this.width ** 2 + this.height ** 2);
}
}
const rect = new Rectangle(3, 4);
console.log(rect.area); // Calculating area... 12
console.log(rect.perimeter); // Calculating perimeter... 14
console.log(rect.diagonal); // 5数据验证和转换
javascript
class User {
constructor() {
this._data = {};
}
get name() {
return this._data.name || 'Anonymous';
}
set name(value) {
if (typeof value !== 'string') {
throw new TypeError('Name must be a string');
}
if (value.length < 2) {
throw new Error('Name must be at least 2 characters long');
}
this._data.name = value.trim();
}
get birthYear() {
return this._data.birthYear;
}
set birthYear(value) {
const currentYear = new Date().getFullYear();
if (value < 1900 || value > currentYear) {
throw new Error(`Birth year must be between 1900 and ${currentYear}`);
}
this._data.birthYear = value;
}
get age() {
if (!this._data.birthYear) return undefined;
return new Date().getFullYear() - this._data.birthYear;
}
}
const user = new User();
user.name = ' Alice '; // 自动去除空格
user.birthYear = 1990;
console.log(user.name); // Alice
console.log(user.age); // 33 (假设当前年份是 2023)缓存机制
javascript
class ExpensiveCalculation {
constructor() {
this._cache = new Map();
}
get fibonacci() {
if (!this._cache.has('fibonacci')) {
console.log('Calculating Fibonacci...');
const result = this._calculateFibonacci(100);
this._cache.set('fibonacci', result);
}
return this._cache.get('fibonacci');
}
_calculateFibonacci(n) {
if (n <= 1) return n;
let a = 0, b = 1;
for (let i = 2; i <= n; i++) {
[a, b] = [b, a + b];
}
return b;
}
return b;
}
// 清除缓存的 setter
set fibonacci(clear) {
if (clear === null) {
this._cache.delete('fibonacci');
console.log('Fibonacci cache cleared');
}
}
}
const calc = new ExpensiveCalculation();
console.log(calc.fibonacci); // Calculating Fibonacci... 354224848179261915075
console.log(calc.fibonacci); // 354224848179261915075 (从缓存获取)
calc.fibonacci = null; // 清除缓存
console.log(calc.fibonacci); // Calculating Fibonacci... 354224848179261915075与 Object.defineProperty 的对比
现代写法 vs 传统写法
javascript
// 传统写法:Object.defineProperty
function createPersonTraditional(name, age) {
const person = {};
Object.defineProperty(person, 'name', {
value: name,
writable: true,
enumerable: true,
configurable: true
});
Object.defineProperty(person, 'age', {
get: function() {
return this._age;
},
set: function(value) {
if (value < 0) throw new Error('Age cannot be negative');
this._age = value;
},
enumerable: true,
configurable: true
});
person._age = age;
return person;
}
// 现代写法:Class getter/setter
class PersonModern {
constructor(name, age) {
this._name = name;
this.age = age; // 使用 setter
}
get name() {
return this._name;
}
set name(value) {
this._name = value;
}
get age() {
return this._age;
}
set age(value) {
if (value < 0) throw new Error('Age cannot be negative');
this._age = value;
}
}
// 对象字面量写法
const createPersonLiteral = (name, age) => ({
_name: name,
_age: age,
get name() {
return this._name;
},
set name(value) {
this._name = value;
},
get age() {
return this._age;
},
set age(value) {
if (value < 0) throw new Error('Age cannot be negative');
this._age = value;
}
});
// 使用对比
const person1 = createPersonTraditional('Alice', 30);
const person2 = new PersonModern('Bob', 25);
const person3 = createPersonLiteral('Charlie', 35);
console.log(person1.name, person1.age); // Alice 30
console.log(person2.name, person2.age); // Bob 25
console.log(person3.name, person3.age); // Charlie 35实际应用场景
响应式系统实现
javascript
// 简单的响应式系统
class ReactiveObject {
constructor(data) {
this._data = {};
this._callbacks = {};
// 为每个属性创建 getter/setter
for (let key in data) {
this._data[key] = data[key];
Object.defineProperty(this, key, {
get: () => {
console.log(`Getting ${key}`);
return this._data[key];
},
set: (value) => {
console.log(`Setting ${key} to ${value}`);
const oldValue = this._data[key];
this._data[key] = value;
// 触发回调
if (this._callbacks[key]) {
this._callbacks[key].forEach(callback => callback(value, oldValue));
}
},
enumerable: true,
configurable: true
});
}
}
// 监听属性变化
watch(property, callback) {
if (!this._callbacks[property]) {
this._callbacks[property] = [];
}
this._callbacks[property].push(callback);
}
}
const reactiveObj = new ReactiveObject({ name: 'Alice', age: 30 });
reactiveObj.watch('name', (newVal, oldVal) => {
console.log(`Name changed from ${oldVal} to ${newVal}`);
});
reactiveObj.name = 'Bob';
// Setting name to Bob
// Name changed from Alice to Bob配置对象管理
javascript
class ConfigManager {
constructor() {
this._config = {};
}
// 通过 getter/setter 管理配置
get theme() {
return this._config.theme || 'light';
}
set theme(value) {
const validThemes = ['light', 'dark', 'auto'];
if (!validThemes.includes(value)) {
throw new Error(`Invalid theme. Valid themes: ${validThemes.join(', ')}`);
}
this._config.theme = value;
}
get language() {
return this._config.language || 'en';
}
set language(value) {
const validLanguages = ['en', 'zh', 'es', 'fr'];
if (!validLanguages.includes(value)) {
throw new Error(`Invalid language. Valid languages: ${validLanguages.join(', ')}`);
}
this._config.language = value;
}
// 获取完整配置
get config() {
return { ...this._config };
}
}
const config = new ConfigManager();
config.theme = 'dark';
config.language = 'zh';
console.log(config.theme); // dark
console.log(config.language); // zh
console.log(config.config); // { theme: 'dark', language: 'zh' }最佳实践
1. 合理使用 getter/setter
javascript
class GoodExample {
constructor(data) {
this._data = data;
}
// 适合使用 getter 的场景:计算属性
get itemCount() {
return this._data.items ? this._data.items.length : 0;
}
// 适合使用 setter 的场景:数据验证
set name(value) {
if (!value || typeof value !== 'string') {
throw new Error('Name must be a non-empty string');
}
this._data.name = value.trim();
}
get name() {
return this._data.name;
}
}
class BadExample {
constructor() {
this._value = 0;
}
// 避免在简单属性上过度使用 getter/setter
get value() { // 不必要的复杂性
return this._value;
}
set value(val) { // 不必要的复杂性
this._value = val;
}
}2. 注意性能影响
javascript
class PerformanceAware {
constructor() {
this._cache = new Map();
this._accessCount = 0;
}
// 对于计算开销大的属性,使用缓存
get expensiveProperty() {
if (!this._cache.has('expensiveProperty')) {
console.log('计算昂贵的属性...');
// 模拟昂贵的计算
const result = Array.from({length: 1000000}, (_, i) => i).reduce((a, b) => a + b);
this._cache.set('expensiveProperty', result);
}
return this._cache.get('expensiveProperty');
}
// 对于简单属性,避免不必要的 getter/setter
simpleProperty = 'simple value';
}3. 保持一致性
javascript
class ConsistentAPI {
constructor(initialValue) {
this._value = initialValue;
}
// 如果使用 getter,也应该提供 setter(如果适用)
get value() {
return this._value;
}
set value(newValue) {
this._value = newValue;
}
// 或者两者都不使用,直接使用普通属性
// otherValue = 'other value';
}总结
getter 和 setter 以及 Object.defineProperty 提供了强大的属性控制能力,允许我们创建具有自定义行为的属性。现代 ES6 class 语法使得 getter/setter 的使用更加简洁和直观,而 Object.defineProperty 则提供了更细粒度的控制。理解这些特性的使用场景和最佳实践,有助于我们创建更加健壮、灵活和易于维护的 JavaScript 代码。通过合理使用这些特性,我们可以实现数据验证、计算属性、缓存机制等高级功能,提升代码的质量和可维护性。