Proxy 和 Object.defineProperty

91 阅读4分钟

Object.defineProperty

1. Object.defineProperty 可以在对象上定义新属性或修改已有属性,并精确控制这些属性的行为

Object.defineProperty(object, key, descriptor)
  • object: 需要定义属性的对象
  • key: 定义的属性名
  • descriptor: 属性描述符

2. descriptor 属性描述符的值

  • value: 设置属性的值
  • writable: 值是否可以重写
  • set: 目标属性设置
  • get: 目标属性获取
  • enumerable: 目标属性是否可以被枚举(是否可遍历)
  • configurable: 目标属性是否可以被删除或是否可以再次修改特性

定义一个不可变的属性:

const obj = {};

Object.defineProperty(obj, 'constant', {
  value: 'I am constant',
  writable: false,
});

console.log(obj.constant); // 输出: I am constant
// 尝试修改该属性将不会有效果,并在严格模式下抛出错误
obj.constant = 'I want to change'; // 在非严格模式下无效,在严格模式下抛出错误

定义一个带有getter和setter的属性:

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

Object.defineProperty(person, 'fullName', {
  get() {
    return this.firstName + ' ' + this.lastName;
  },
  set(value) {
    [this.firstName, this.lastName] = value.split(' ');
  }
});

console.log(person.fullName); // John Doe
person.fullName = 'Jane Doe';
console.log(person.firstName); // Jane
console.log(person.lastName); // Doe

注意事项

  1. 默认值:如果属性描述符中省略了 writable、enumerable 和 configurable 这三个属性,它们的默认值都是 false。这意味着如果没有明确指定,属性将不可写、不可枚举且不可配置。
  2. 不可变的属性:一旦属性被定义为 writable: false,你就不能再更改该属性的值,除非你再次使用 Object.defineProperty() 方法并改变 writable 为 true。
  3. 性能考虑:虽然 Object.defineProperty() 提供了很大的灵活性,但是如果对大量对象或在性能敏感的场合使用它,需要注意其可能带来的性能影响。特别是动态的存取器(getter和setter),可能会因为每次访问属性都需要执行额外的代码而降低性能。
  4. 不可配置性:一旦将属性的 configurable 设置为 false,你将无法更改 enumerable、configurable 以及 writable 属性,也无法将该属性从对象上删除。此操作是不可逆的,因此需要谨慎使用。
  5. 如果在 set 里面直接为 obj 需要定义的属性赋值,会形成一个死循环,所以需要一个变量来代替 obj 的属性值进行存储和充当返回值
const obj = {};

Object.defineProperty(obj, 'name', {
  get() {
    return this.name;
  },
  set(value) {
    this.name = value;
  }
});

obj.name = 2;

image.png

Proxy

Proxy 是一个内置的构造函数,用于创建一个对象的代理(proxy),从而可以在基本操作(如属性查找、赋值、枚举、函数调用等)之前拦截并定义自定义行为。

const proxy = new Proxy(target, handler);
  • target: 要使用代理包装的原始对象。
  • handler: 一个通常以函数作为属性的对象,这些函数定义了在执行各种操作时代理的行为。 -handler 对象

handler 对象是由一系列“陷阱”(trap)组成的,这些陷阱是函数,当执行某些基本操作时会被调用。下面是一些常用的陷阱:

  • get(target, prop, receiver): 拦截对象属性的读取操作。
  • set(target, prop, value, receiver): 拦截对象属性的设置操作。
  • has(target, prop): 拦截 prop in proxy 的操作,以及相关的 Reflect.has()。
  • deleteProperty(target, prop): 拦截删除对象属性的操作。
  • apply(target, thisArg, argumentsList): 拦截函数调用。
  • construct(target, argumentsList, newTarget): 拦截 new 操作符。
const target = {
  message: 'Hello, world!'
};

const handler = {
  get(obj, prop){
    if (prop in obj) {
      return obj[prop];
    }
    return `Property ${prop} doesn't exist.`;
  },

  set(obj, prop, value) {
    // 你可以添加额外的逻辑进行验证或转换等
    if (typeof value !== 'string') {
      throw new TypeError('Value must be a string.');
    }
    obj[prop] = value;
    // 通常情况下返回 true 表示设置成功
    return true;
  }
};

const proxy = new Proxy(target, handler);

console.log(proxy.message); // Hello, world!
console.log(proxy.nonExistent); // Property nonExistent doesn't exist.
proxy.message = 'Hello, Proxy!'; // 成功设置新的值
proxy.another = 123; // 抛出 TypeError: Value must be a string.

处理 Map 或者 Set

const m = new Map(); // const m = new Set()

const proxy = new Proxy(m, {
  get(target, key){
    // 如果是方法,修正 this 指向
    let value = target[key];
    if(value instanceof Function){
      retrun value.bind(target);
    }
    return value;
  },
  set(target, key, value){
    target[key] = value;
  }
})

注意事项

  • Proxy 是不透明的,在外部不能直接判断一个对象是否是 Proxy。
  • 不是所有的对象操作都可以被拦截,例如修改对象的原型就不能被 Proxy 拦截。
  • Proxy 对象具有一定性能影响,对于要求性能的场合应谨慎使用。
  • Proxy 内部的一些操作(如 handler.apply 和 handler.construct)必须遵守特定的返回值规约,否则会抛出 TypeError。
  • 一旦代理创建,target 对象将被封装在 Proxy 中,外部对 Proxy 对象的所有操作都会通过 handler 对象来进行。

总之,Proxy 是一个功能强大的特性,可以帮助开发者设计出非常灵活且可定制的行为,用于各种高级抽象、API 包装器、观察者模式、虚拟对象等场景。