Skip to content

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 value

getter 和 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.com

Object.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 代码。通过合理使用这些特性,我们可以实现数据验证、计算属性、缓存机制等高级功能,提升代码的质量和可维护性。