赞
踩
Proxy 意为"代理器",用于修改目标对象的成员之前进行"拦截",外界对该对象的成员的访问,都必须先通过这层拦截。其中,成员包括未定义的,即访问或设置不存在的属性,也会触发相应的代理函数。
通过构造函数可以创建 Proxy 对象(new Proxy()
),需要依次传入两个参数:
下面给出简单的例子:
- let obj = { a: 1 }
- let proxy = new Proxy(obj, {
- get(obj, prop) {
- return 'proxyReturn'
- }
- });
- console.log(proxy.a); // 'proxyReturn'
- console.log(proxy.b); // 'proxyReturn'复制代码
例子中,proxy
对obj
对象拦截,访问其属性时,永远返回的是'proxyReturn'
,即使属性是不存在。而get
函数是众多代理函数中的一种,下面会分别介绍其他的代理函数。
有一种情况,Proxy 对象作为某个对象的原型。看下面例子:
- let proxy = new Proxy({}, {
- get(obj, prop) {
- return 'proxyReturn'
- }
- });
- let obj = { a: 1 }
- Object.setPrototypeOf(obj, proxy);
- console.log(obj.a); // 1
- console.log(obj.b); // 'proxyReturn'复制代码
当访问obj.b
时,由于b
在obj
对象内不存在,因此会到访问原型是否存在该属性,即访问了proxy
对象,从而触发了代理函数。
代理对象必须与被代理的对象具有相同特性(这里说的特性指的是,对象是否冻结,是否密封,是否可扩展),即被代理的对象不能修改,使用代理对象的set
方法也会报错。再例如,被代理的对象被冻结,get
方法不能返回与原值不同的值;getOwnPropertyDescriptor
必须返回描述符对象。总之一句,被代理对象不能改的,代理对象同样不能改;代理对象的方法的返回值类型必须与被代理对象一致。
控制器对象内方法的this
指向控制器对象本身,看如下例子:
- let handler = {
- get() {
- return this
- }
- }
- let proxy = new Proxy({}, handler);
- console.log(proxy.a === handler); // true复制代码
上面提到,控制器对象可以包含零个或多个的代理函数。像get
这样的代理函数,一共有 13 个。简单介绍下面出现的参数名意义:
get
方法用于拦截某个成员的读取操作(包括方法),上面已经简单给出了例子。下面给出代理数组的例子,使其可以用负数索引。
- let arr = [1, 2, 3, 4, 5];
- let proxy = new Proxy(arr, {
- get(target, prop) {
- let length = target.length,
- index = Number.parseInt(prop);
- if (index < 0) {
- return target[length + index]
- }
- if (index >= length) {
- return target[length - 1]
- }
- return target[index]
- }
- });
- console.log(proxy[-1]); // 5
- console.log(proxy[10]); // 5
- console.log(proxy[2]); // 3复制代码
set
方法用于拦截对象属性的设置。其中,value
表示属性的目标值。比较实用的用法是,与 DOM 的数据绑定。
需要注意的是!当存在二级属性时,如[{}, {}]
或{ a: {}, b: {} }
,对二级或以上的属性修改并不会触发set
方法。
如下例子:
- let listData = ['one', 'two', 'three'];
- let proxy = new Proxy(listData, {
- set(target, prop, value) {
- let dom = document.getElementsByClassName('list-item')[prop]
- dom ? dom.innerText = value : null;
- target[prop] = value;
- }
- })
- proxy[0] = 'changed-one';复制代码
has
方法用来拦截HasProperty
操作,即判断对象是否具有某个属性时,这个方法会生效。典型的操作就是in
运算符。
注意,hasOwnProperty()
和for...in
都不能触发has
方法。
注意,has
方法只能返回布尔值,return
非布尔值会自动转成布尔值。
- let arr = [1, 2, 3, 4];
- let proxyArr = new Proxy(arr, {
- has(target, prop) {
- return 'a'
- }
- })
- console.log('x' in proxyArr); // true复制代码
deleteProperty
方法用于拦截delete
操作,只能返回布尔值,非布尔值会被转成布尔值,代表属性是否被删除成功,省略默认返回false
。
- let obj = { a: 1, b: 2 }
- let proxyDel = new Proxy(obj, {
- deleteProperty(target, prop) {
- delete target[prop]
- }
- });
- console.log(delete proxyDel.a); // false
- console.log(obj); // { b: 2 }复制代码
getOwnPropertyDescriptor
方法拦截Object.getOwnPropertyDescriptor()
或Object.getOwnPropertyDescriptors()
,返回一个属性描述对象或者undefined
。
注意,若返回值不是undefined
,则返回值必须是包含[[configurable]]
为true
的对象,该值为false
或省略会导致报错;若其他描述符未定义,以默认值填充;若返回对象包含除描述符以外的属性,则该属性被忽略。
- // undefined
- let proxy1 = new Proxy({}, {
- getOwnPropertyDescriptor(target, prop) {
- return undefined
- }
- });
- console.log(Object.getOwnPropertyDescriptor(proxy1, 'a'));
- // { value: undefined, writable: false, enumerable: false, configurable: true }
- let proxy2 = new Proxy({}, {
- getOwnPropertyDescriptor(target, prop) {
- return { configurable: true }
- }
- });
- console.log(Object.getOwnPropertyDescriptor(proxy2, 'a'));
- // 报错
- let proxy3 = new Proxy({}, {
- getOwnPropertyDescriptor(target, prop) {
- return { configurable: false }
- }
- });
- console.log(Object.getOwnPropertyDescriptor(proxy3, 'a'));复制代码
defineProperty
方法拦截Object.defineProperty()
、Object.defineProperties()
、obj.prop = 'value'
等修改/添加对象的属性的操作。该方法只能返回布尔值,非布尔值会转成布尔值,表示是否成功定义属性,返回fasle
会导致报错。其中,propDesc
表示属性的描述符。
注意,如果对象被冻结(Object.freeze()
)或对象被密封(Object.seal()
)或对象不可扩展( Object.preventExtensions()
),甚至是某个属性被密封([[configurable]]
特性为false
),对代理对象使用Object.defineProperty()
都可能会报错。总之,和原对象的特性一致,不能被修改的被代理后同样不能被修改。
- let obj = { d: 4 }
- let proxyObj = new Proxy(obj, {
- defineProperty() {
- console.log('proxy-defineProperty');
- return true
- }
- })
- // 以下4种方法都会触发 defineProperty 方法
- Object.defineProperty(proxyObj, 'a', {});
- Object.defineProperties(proxyObj, { b: {} });
- proxyObj.c = 3;
- proxyObj.d = 5;复制代码
apply
方法拦截函数的调用、call
、apply
、bind
操作。其中,object
表示函数上下文对象,args
表示函数的参数。
- let obj = { a: 10 }
- function fn() { }
- let proxyFn = new Proxy(fn, {
- apply(target, object, args) {
- console.log('proxyFn-apply');
- }
- })
- // 直接执行函数 会触发
- proxyFn();
- // apply 和 call 会触发
- proxyFn.apply(obj);
- proxyFn.call(obj);
- // bind 定义时不会触发 执行时触发
- let p = proxyFn.bind(obj);
- p();复制代码
construct
方法用于拦截new
命令。
注意,被代理的函数不能是箭头函数,否则也会报错。
注意,construct
方法必须要return
对象(包括函数、数组等),否则会报错。
- // 错误例子1
- let P1 = new Proxy(()=>{},{
- construct(){
- return {}
- }
- });
- let p1 = new P1(); // 报错,被代理函数不能是箭头函数
- // 错误例子2
- let P2 = new Proxy(function(){},{
- construct(){}
- });
- let p2 = new P2(); // 报错,construct必须返回对象复制代码
getPrototypeOf
方法主要用来拦截获取对象原型,返回值必须是null
或对象。具体来说,拦截下面这些操作。
Object.prototype.__proto__
// 只能在浏览器环境下执行Object.prototype.isPrototypeOf()
// 代理对象作为参数Object.getPrototypeOf()
// 代理对象作为参数Reflect.getPrototypeOf()
// 代理对象作为参数instanceof
// 代理对象在运算符的左边下面给出简单例子:
- let proxy = new Proxy({},{
- getPrototypeOf(){
- console.log('proxy-getPrototypeOf');
- return null
- }
- });
- // 都会打印 'proxy-getPrototypeOf'
- let x1 = proxy.__proto__; // 只能在浏览器下执行
- let x2 = {}.isPrototypeOf(proxy);
- let x3 = Object.getPrototypeOf(proxy);
- let x4 = Reflect.getPrototypeOf(proxy);
- let x5 = (proxy instanceof function(){});复制代码
setPrototypeOf
方法主要用来拦截Object.setPrototypeOf()
方法。和Object.setPrototypeOf()
方法一样,必须返回被设置的对象,否则报错。其中,proto
表示被设为原型的对象。
注意,浏览器环境下可以通过__proto__
设置原型对象,同样能触发setPrototypeOf
方法。
- let proxy = new Proxy({}, {
- setPrototypeOf(target, proto) {
- console.log('proxy-setPrototypeOf')
- return target;
- }
- });
- // 都打印 'proxy-setPrototypeOf'
- Object.setPrototypeOf(proxy, {});
- proxy.__proto__ = {} // 非浏览器环境下使用会报错复制代码
isExtensible
方法拦截Object.isExtensible()
方法,必须返回布尔值,非布尔值会被转成布尔值,且返回值必须与源对象所对应的值相同,否则报错。
- // 错误示范
- let proxy = new Proxy({}, {
- isExtensible() {
- return false
- }
- });
- console.log(Object.isExtensible(proxy));复制代码
preventExtensions
方法拦截Object.preventExtensions()
方法,只能返回被设为不可扩展的被代理对象,否则报错。如下:
- let proxy = new Proxy({}, {
- preventExtensions(target) {
- console.log('proxy-preventExtensions')
- return Object.preventExtensions(target);
- }
- });
- Object.preventExtensions(proxy); // proxy-preventExtensions
- // 错误示范
- let proxyErr = new Proxy({}, {
- preventExtensions(target) {
- return {}
- }
- });
- Object.preventExtensions(proxyErr); // 报错复制代码
ownKeys
方法用来拦截对象自身所有属性的读取操作,必须返回数组或对象,否则报错。具体来说,拦截以下操作。
Object.getOwnPropertyNames()
。Object.getOwnPropertySymbols()
。getOwnPropertyDescriptors()
。Object.entries()
、Object.keys()
、Object.values()
。for...in
循环。注意,虽然可以返回非数组的对象,对于如Object.keys
这种默认返回数组的方法,得到的结果会是一个空数组。
注意,诸如Object.entries
方法返回的数组不会含有Symbol
属性名、不存在的属性名、不可遍历(enumerable
)的属性名,与被代理对象的行为一致。Object.getOwnPropertySymbols
及其他方法同理,与被代理对象一致。
看下面简单例子:
- let proxy = new Proxy({ a: 1, b: 2 }, {
- ownKeys() {
- console.log('proxy-ownKeys');
- return ['a', 'c']
- }
- });
- // 都会打印 'proxy-ownKeys'
- let x1 = Object.entries(proxy);
- let x2 = Object.keys(proxy);
- let x3 = Object.values(proxy);
- for (let x4 in proxy) { }
- let x5 = Object.getOwnPropertyNames(proxy);
- let x6 = Object.getOwnPropertyDescriptors(proxy);
- let x7 = Object.getOwnPropertySymbols(proxy);
- // entries、keys、values 都会过滤不可能显示的属性,跟被代理对象一致
- console.log(x1); // [ [ 'a', 1 ] ]
- 复制代码
Proxy.revocable()
方法与new Proxy()
用法类似,都是依次传入被代理对象和控制器对象,返回一个包含proxy
对象和revoke
方法的对象。其中,proxy
对象和上面描述的一致,revoke
方法用于解除proxy
对象的代理,一旦取消将无法继续使用。
- let proxyR = Proxy.revocable({ a: 1 }, {});
- console.dir(proxyR); // { proxy: { a: 1 }, revoke: [Function] }
- console.log(proxyR.proxy); // { a: 1 }
- proxyR.revoke();
- // console.log(proxyR); // 报错复制代码
作者:hozee的笔记
链接:https://juejin.im/post/5ce69c926fb9a07ed657ac43
来源:掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。