proxy能捕获到属性的添加和删除吗?讲讲proxy的那些事!

260 阅读6分钟

代理捕获器与反射方法

一道面试题让我紧急回看红宝书proxy能捕获到属性的添加和删除操作吗?

代理可以捕获13种不同的基本操作。这些操作有各自不同的反射API方法、参数、关联ECMAScript操作和不变式

只要在代理上调用,所有捕获器都会拦截它们对应的反射API操作。

get

get捕获器会在获取值的操作中被调用。对应的反射API方法为Reflect.get()。

const myTarget = {};

Object.defineProperty(myTarget, 'name', {
  value: 'zby',
})

const proxy = new Proxy(myTarget, {
  get(target, property, receiver) {
    console.log('get()');
    return Reflect.get(...arguments);
  }
});

// 拦截的操作
// 1. proxy.property
proxy.foo;// get()

// 2. proxy[property]
proxy['foo'];// get()

// 3. Object.create(proxy)[property] - Object.create()静态方法接收传入的对象作为新对象原型
const newObj = Object.create(proxy);
newObj['foo'];// get() - 对象本身没有这个属性,读取到原型时触发代理对象的捕获操作
newObj.foo = 'bar';// 给对象本身添加这个属性
newObj.foo;// 这样就不会触发了。

// 4. Reflect.get(proxy, property[, receiver])
Reflect.get(proxy, 'foo');// get()

捕获器的第三个参数receiver我们直觉上可能会觉得就是代理对象,这是不完全的!看看完全的说法:

receiver: 代理对象或继承代理的对象

如果target对象中指定了getterreceiver则为getter调用时的this值。

  1. 代理对象的情况比较好理解:
const myTarget = {
  name: 'myTarget',
};

const proxy = new Proxy(myTarget, {
  get(target, property, receiver) {
    console.log(receiver === proxy);// true
    return target[property];
  }
});

console.log(proxy.name);// myTarget

和我们预想的一样,receiver这里就是代理对象,这没什么好说的。

  1. 看一下继承代理的对象是什么情况:
const myTarget = {
  name: 'myTarget',
  get value() {
    return this.name;
  }
};

const proxy = new Proxy(myTarget, {
  get(target, property, receiver) {
    console.log(receiver === obj);// true
    return target[property];
  }
});

const obj = Object.create(proxy, {
  name: {
    value: 'obj',
  }
})

console.log(obj.value);// myTarget

没有翻车,这里的receiver就是obj(继承代理对象的对象)。那么又有一个问题,receiver参数有什么用,就是为了让我们验证这个参数吗?当然不是!

仔细看一下:输出结果是myTargetobj有自身的name属性,这里不应该优先获取自身的属性吗?

receiver的作用这不来了吗! 研究一下程序的执行流,最终是触发了原生对象的[[get]]方法,这个方法返回的是this.name。哦!到这里就有眉目了,大概是this出问题了,这里的this指向myTarget,实际上应该是obj吧!去哪找正确的this呢?刚才的receiver派上用场了。

我灵机一动:

return receiver[property];

好家伙,亏你想的出来,直接递归了,看来不是正解。

反射登场:

const myTarget = {
  name: 'myTarget',
  get value() {
    console.log(this === obj);// true
    return this.name;
  }
};

const proxy = new Proxy(myTarget, {
  get(target, property, receiver) {
    return Reflect.get(...arguments);
  }
});

const obj = Object.create(proxy, {
  name: {
    value: 'obj',
  }
})

console.log(obj.value);// obj

输出结果是objget value()内的this值为obj,也就是get()捕获器中的receiver

这里的反射API传入的第三个参数receiver(来自get()捕获器的第三个参数)修改了属性访问时的this指向为receiver

捕获器不变式

  • 如果target.property不可写且不可配置,则处理程序返回的值必须与target.property匹配。

直接上代码:

const myTarget = {};

Object.defineProperty(myTarget, 'name', {
  value: 'zby',
})

const proxy = new Proxy(myTarget, {
  get(target, property, receiver) {
    return 'cxy';
  }
});

proxy.name;

使用Object.definePropertymyTarget对象添加了一个属性name,值为zby; proxy捕获属性的读取操作返回cxy。报错如下:

image.png 只读且不可配置的属性,要求返回其真实值也无可厚非。

  • 如果target.property不可配置且[[Get]]特性为undefined,处理程序的返回值也必须是undefined。
const myTarget = {};

Object.defineProperty(myTarget, 'name', {
  get: undefined,
})

const proxy = new Proxy(myTarget, {
  get(target, property, receiver) {
    return 'cxy';
  }
});

proxy.name;

image.png

set

set捕获器会在设置属性值的操作中被调用。对应的反射API方法为Reflect.set()。

const myTarget = {};

const proxy = new Proxy(myTarget, {
  set(target, property, value, receiver) {
    console.log('set()');
    return Reflect.set(...arguments);
  }
});

proxy.foo = 'bar';// set()
proxy['foo'] = 'bar';// set()
Object.create(proxy)['foo'] = 'baz';// set()
Reflect.set(proxy, 'foo', 'bar');// set()

捕获器不变式:

  • 如果target.property不可写且不可配置,则不能修改目标属性的值。
  • 如果target.property不可配置且[[Set]]特性为undefined,则不能修改目标属性的值。

has

has()捕获器会在in操作符中被调用。

const myTarget = {};

const proxy = new Proxy(myTarget, {
  has(target, property) {
    console.log('has()');
    return Reflect.has(...arguments);
  }
});

// 拦截的操作
'foo' in proxy;
'foo' in Object.create(proxy);
Reflect.has(proxy, 'foo');

... 后续捕获操作的捕获器不变式就不列在文档里了,一些特殊的情况大家自行查阅文档。

defineProperty

defineProperty()捕获器会在Object.defineProperty()中被调用。对应的反射API方法为Reflect.defineProperty()。

const myTarget = {};

const proxy = new Proxy(myTarget, {
  defineProperty(target, property, descriptor) {
    console.log('defineProperty()');
    return Reflect.defineProperty(...arguments);
  }
});

Object.defineProperty(proxy, 'foo', { value: 'bar' });// defineProperty()
Reflect.defineProperty(proxy, 'foo', { value: 'bar' });// defineProperty()

getOwnPropertyDescriptor

该捕获器会在Object.getOwnPropertyDescriptor()中被调用。 反射API与其同名。

Object.getOwnPropertyDescriptor() - 取得指定属性的属性描述符 两个参数:属性所在的对象和要取得其描述符的属性

const myTarget = {};

const proxy = new Proxy(myTarget, {
  defineProperty(target, property, descriptor) {
    console.log('defineProperty()');
    return Reflect.defineProperty(...arguments);
  }
});

Object.defineProperty(proxy, 'foo', { value: 'bar' });// defineProperty()
Reflect.defineProperty(proxy, 'foo', { value: 'bar' });// defineProperty()

deleteProperty

deleteProperty()捕获器会在delete操作符中被调用。

const myTarget = {};

const proxy = new Proxy(myTarget, {
  deleteProperty(target, property) {
    console.log('deleteProperty()');
    return Reflect.deleteProperty(...arguments);
  }
});

console.log(delete proxy.foo);

这个捕获器必须返回布尔值,表示删除属性是否成功。返回非布尔值会被转换为布尔值。

捕获器不变式:

  • 如果自有属性存在且不可配置,处理程序不能删除这个属性。(捕获器返回false)
Object.defineProperty(myTarget, 'name', {
  value: 'cxy',
})

console.log(delete proxy.name);
console.log(proxy.name);
// deleteProperty()
// false
// cxy

ownKeys

ownKeys()捕获器会在Object.keys()及类似方法中被调用。

Object.keys()  静态方法返回一个由给定对象自身的可枚举的字符串键属性名组成的数组。

const myTarget = {};


const proxy = new Proxy(myTarget, {
  ownKeys(target) {
    console.log('ownKeys()');
    return Reflect.ownKeys(...arguments);
  }
});

Object.keys(proxy);// ownKeys()
Object.getOwnPropertyNames(proxy);// ownKeys()
Object.getOwnPropertySymbols(proxy);// ownKeys()
Reflect.ownKeys(proxy);// ownKeys()

getPrototypeOf

捕获器会在Object.getPrototypeOf()中被调用。

Object.getPrototypeOf()

  • 返回参数的内部特性__proto__的值
  • 该方法可以方便地取得一个对象的原型,这在通过原型实现继承时显得尤为重要
const myTarget = {};

const proxy = new Proxy(myTarget, {
  getPrototypeOf(target) {
    console.log('getPrototypeOf()');
    return Reflect.getPrototypeOf(...arguments);
  }
});

Object.getPrototypeOf(proxy);// getPrototypeOf()

proxy.__proto__;// getPrototypeOf()

Object.prototype.isPrototypeOf(proxy);// getPrototypeOf()

proxy instanceof Object;// getPrototypeOf()

Reflect.getPrototypeOf(proxy);// // getPrototypeOf()

setPrototypeOf

setPrototypeOf()捕获器会在Object.setPrototype()中被调用。

Object.setPrototypeOf() (该方法不推荐使用,可能造成性能问题或未知bug) 静态方法可以将一个指定对象的原型(即内部的 [[Prototype]] 属性)设置为另一个对象或者 null。

const myTarget = {};

const proxy = new Proxy(myTarget, {
  setPrototypeOf(target, prototype) {
    console.log('setPrototypeOf()');
    return Reflect.setPrototypeOf(...arguments);
  }
});

const test = {
  name: 'Matt'
};

Object.setPrototypeOf(proxy, test);// setPrototypeOf()
console.log(proxy.name);// console.log(proxy.name);Matt
console.log(myTarget.name);// console.log(myTarget.name);Matt

isExtensible

isExtensible()捕获器会在Object.isExtensible()中被调用。

Object.isExtensible()  静态方法判断一个对象是否是可扩展的(是否可以在它上面添加新的属性)。

const myTarget = {};

const proxy = new Proxy(myTarget, {
  isExtensible(target, prototype) {
    console.log('isExtensible()');
    return Reflect.isExtensible(...arguments);
  }
});

Object.isExtensible(proxy);// isExtensible()

如果target可扩展,处理程序必须返回true; 如果target不可扩展,处理程序必须返回false。

const proxy = new Proxy(myTarget, {
  isExtensible(target, prototype) {
    console.log('isExtensible()');
    return false;
  }
});

image.png 可扩展的对象手动返回false会报错

preventExtensions

Object.preventExtensions()中被调用。

Object.preventExtensions()  静态方法可以防止新属性被添加到对象中(即防止该对象被扩展)。它还可以防止对象的原型被重新指定。

const myTarget = {};

const proxy = new Proxy(myTarget, {
  preventExtensions(target) {
    console.log('preventExtensions()');
    return Reflect.preventExtensions(...arguments);
  }
});

Object.preventExtensions(proxy);

proxy.name = 'name';// preventExtensions()
console.log(proxy.name);// undefined

apply

apply()捕获器会在调用函数时被调用。此时target必须是一个函数

const myTarget = () => {};

const proxy = new Proxy(myTarget, {
  apply(target, thisArg, argumentsList) {
    // thisArg: 调用函数时的this参数
    // argumentsList: 调用函数时的参数列表
    console.log('apply()');
    console.log(argumentsList);
    return Reflect.apply(...arguments);
  }
});

proxy(1, 2, 3);
// apply()
// [ 1, 2, 3 ]

construct

construct()捕获器会在new操作符中被调用。

const myTarget = function () { };

const proxy = new Proxy(myTarget, {
  construct(target, argumentsList, newTarget) {
    console.log('construct()');
    return Reflect.construct(...arguments);
  }
});

new proxy;

总结

这样的话,开篇的问题也就有了答案。proxy可以捕获到属性的添加和删除操作, 对应的捕获器为defineProperty()deleteProperty()

欢迎大家在评论区留言。

不到长城非好汉,屈指行程二万。