Property Descriptors

理解和使用 JavaScript 属性描述符

问题

JavaScript 中每个对象属性都有一个”属性描述符”,它控制着属性的行为。如何获取、定义和修改属性描述符?

解答

什么是属性描述符

属性描述符是一个对象,描述了属性的特性。分为两种类型:

  1. 数据描述符:有 valuewritable
  2. 访问器描述符:有 getset

两者共有的属性:configurableenumerable

获取属性描述符

const obj = { name: 'Alice' };

// 获取单个属性的描述符
const descriptor = Object.getOwnPropertyDescriptor(obj, 'name');
console.log(descriptor);
// {
//   value: 'Alice',
//   writable: true,
//   enumerable: true,
//   configurable: true
// }

// 获取所有属性的描述符
const allDescriptors = Object.getOwnPropertyDescriptors(obj);
console.log(allDescriptors);

定义属性描述符

const obj = {};

// 定义数据描述符
Object.defineProperty(obj, 'name', {
  value: 'Bob',
  writable: false,      // 不可修改
  enumerable: true,     // 可枚举
  configurable: false   // 不可删除或重新配置
});

obj.name = 'Alice';     // 静默失败,严格模式下报错
console.log(obj.name);  // 'Bob'

// 定义访问器描述符
let _age = 18;
Object.defineProperty(obj, 'age', {
  get() {
    return _age;
  },
  set(val) {
    if (val > 0) _age = val;
  },
  enumerable: true,
  configurable: true
});

obj.age = 25;
console.log(obj.age);   // 25

批量定义属性

const obj = {};

Object.defineProperties(obj, {
  firstName: {
    value: 'John',
    writable: true,
    enumerable: true,
    configurable: true
  },
  lastName: {
    value: 'Doe',
    writable: true,
    enumerable: true,
    configurable: true
  },
  fullName: {
    get() {
      return `${this.firstName} ${this.lastName}`;
    },
    enumerable: true,
    configurable: true
  }
});

console.log(obj.fullName); // 'John Doe'

描述符默认值

// 通过字面量创建的属性,默认都是 true
const obj1 = { a: 1 };
// 等同于
Object.defineProperty({}, 'a', {
  value: 1,
  writable: true,
  enumerable: true,
  configurable: true
});

// 通过 defineProperty 创建的属性,默认都是 false
Object.defineProperty(obj1, 'b', { value: 2 });
// 等同于
Object.defineProperty(obj1, 'b', {
  value: 2,
  writable: false,
  enumerable: false,
  configurable: false
});

实际应用:实现简单的响应式

function reactive(obj) {
  const result = {};
  
  Object.keys(obj).forEach(key => {
    let value = obj[key];
    
    Object.defineProperty(result, key, {
      get() {
        console.log(`读取 ${key}: ${value}`);
        return value;
      },
      set(newVal) {
        console.log(`设置 ${key}: ${value} -> ${newVal}`);
        value = newVal;
      },
      enumerable: true,
      configurable: true
    });
  });
  
  return result;
}

const state = reactive({ count: 0 });
state.count;      // 读取 count: 0
state.count = 1;  // 设置 count: 0 -> 1

关键点

  • 数据描述符valuewritable访问器描述符getset,两者互斥
  • enumerable 控制属性是否出现在 for...inObject.keys()
  • configurable: false 后属性不可删除,且不能再修改描述符(writable 从 true 改 false 除外)
  • 字面量创建的属性默认全为 true,defineProperty 创建的默认全为 false
  • Vue 2 的响应式原理就是基于 Object.defineProperty 实现的