> 文章列表 > proxy详细介绍与使用

proxy详细介绍与使用

proxy详细介绍与使用

proxy详细介绍与使用

proxy 对象用于创建一个对象的代理,是在目标对象之前架设一个拦截,外界对该对象的访问,都必须先通过这个拦截。通过这种机制,就可以对外界的访问进行过滤和改写。

ES6 原生提供 Proxy 构造函数,用来生成 Proxy 实例。

var proxy = new Proxy(target, handler);

target参数表示所要拦截的目标对象,handler参数也是一个对象,用来定制拦截行为。

const obj = {};
const proxy=new Proxy(obj,{get:function(target,propKey){return 1}
})console.log(proxy.name) // 1
console.log(proxy.value) // 1

在上面代码中,Proxy接受两个参数,第一个是要代理的目标对象(上面的是空对象obj),在没有设置Proxy的情况下,对proxy的操作就是访问obj。第二个参数是配置对象,对每个被代理的操作,需要提供一个对应的处理函数,这个函数用于拦截对应的操作。

上面代码中,配置对象有个get方法,用来拦截对目标对象属性的访问请求。get方法中的两个参数分别是目标对象和所要访问的属性。由于设置的是返回1,所以访问任何属性都得到1。

如果handler没有设置拦截,就等同于直接通向原对象。

const obj = {};
const proxy=new Proxy(obj, {})
proxy.a = 1
console.log(obj.a)         // 1
console.log(obj === proxy) // false

handler是空对象,没有拦截作用,访问proxy等同于访问obj。但是代理对象和原对象并不相等。

handler配置

handler 对象是一个容纳一批特定属性的占位符对象。它包含有 proxy 的各个捕获器。

1. get

get方法用于拦截某个属性的读取操作,接受三个参数,(目标对象,属性名,Proxy或者proxy实例本身),第三个参数可选。

const obj={value:1
}
const proxy=new Proxy(obj,{get:function(target,propKey){if(propKey in target){return target[propKey]}else{throw new ReferenceError(`${propKey} 不存在`)}}
})
console.log(proxy.value) // 1
proxy.name //报错

如果访问目标对象不存在的属性,会抛出一个错误。如果没有这个拦截函数,访问不存在的属性,只会返回undefined

get方法可以被继承

const obj = {a: 1
};
let proto = new Proxy(obj, {get(target, propertyKey) {return target[propertyKey];}
});
let newObj = Object.create(proto);
newObj.a // "GET foo"

拦截操作定义在原型对象上面,所以如果读取obj对象继承的属性时,拦截会生效。

下面是一个get方法的第三个参数的例子,它总是指向原始的读操作所在的那个对象,一般情况下就是 Proxy 实例。

const proxy = new Proxy({}, {get: function(target, key, receiver) {return receiver;}
});
proxy.getReceiver === proxy // true

对于get方法,proxy存在一些约束

  • 如果要访问的目标属性是不可写以及不可配置的,则返回的值必须与该目标属性的值相同,否则会抛出异常。
    const obj = {};
    Object.defineProperty(obj, "a", {configurable: false,enumerable: false,value: 1,writable: false
    });
    const proxy=new Proxy(obj,{get:function(target,propKey){return 2      // return 1 就没问题}
    });
    proxy.a
    
  • 如果要访问的目标属性没有配置访问方法,即 get 方法是 undefined 的,则返回值必须为 undefined
    const obj = {};
    Object.defineProperty(obj, "a", {configurable: false,get: undefined
    });
    const proxy=new Proxy(obj,{get:function(target,propKey){return 1;        // return undefined 就没问题}
    });
    proxy.a
    

2. set

用来拦截某个属性的赋值操作,接受四个参数(目标对象、属性名、属性值、最初被调用的对象(通常是proxy实例本身,但 handlerset 方法也有可能在原型链上,或以其他方式被间接地调用(因此不一定是 proxy 本身))),第四个参数可选。

const proxy = new Proxy({}, {set: function(obj,prop,value){if(prop === 'value') {if(!Number.isInteger(value)){throw new TypeError('不是整数')}if(value>10){throw new RangeError('太大了')}}obj[prop] = value}
})
proxy.value = 5
proxy.value = 'val'     //报错
proxy.value = 20        //报错

由于设置了存值函数set,任何不符合要求的value属性赋值,都会抛出一个错误,这是数据验证的一种实现方法。

来看看set在原型链上的情况.

const proxy = new Proxy({}, {set: function(obj, prop, value, receiver) {console.log(receiver === newObj);     // trueif(prop === 'value') {if(!Number.isInteger(value)){throw new TypeError('不是整数')}if(value>10){throw new RangeError('太大了')}}obj[prop] = value}
})
let newObj = Object.create(proxy);
newObj.value = 1;

将代理对象作为原型创建一个新对象,此时第四个参数就是这个新对象而不是proxy实例。

对于set方法,proxy也存在一些约束

  • 若目标属性是一个不可写及不可配置的数据属性,则不能改变它的值。
    const obj = {};
    Object.defineProperty(obj, "a", {configurable: false,enumerable: false,value: 1,writable: false
    });
    const proxy=new Proxy(obj,{set: function(obj, prop, value) {obj[prop] = value}
    });
    proxy.a = 1;
    proxy.a       // 还是 undefined
    
  • 如果目标属性没有配置存储方法,即 set 的是 undefined,则不能设置它的值.
    const obj = {};
    Object.defineProperty(obj, "a", {configurable: false,set: undefined
    });
    const proxy=new Proxy(obj,{set: function(obj, prop, value) {obj[prop] = value}
    });
    proxy.a = 1;
    proxy.a       // 还是 undefined
    
  • 在严格模式下,如果 set 方法返回 false,那么也会抛出一个异常。

3. apply

拦截函数的调用,接受三个参数,分别是目标对象、目标对象的上下文对象(this)和目标对象的参数数组。

var target = function () { return 'target'; };
var handler = {apply: function () {return 'proxy';}
};var p = new Proxy(target, handler);
p()
// "proxy"

变量pProxy 的实例,当它作为函数调用时(p()),就会被apply方法拦截,返回一个字符串。

4. has

用来拦截HasProperty操作,即判断对象是否具有某个属性时,这个方法会生效。典型的操作就是in运算符。接受两个参数,分别是目标对象、需查询的属性名。

var handler = {has (target, key) {return key in target;}
};
var target = { prop: 'foo' };
var proxy = new Proxy(target, handler);
'propa' in proxy // false

如果原对象不可配置或者禁止扩展,这时has拦截会报错。

var obj = { a: 10 };
Object.preventExtensions(obj);var p = new Proxy(obj, {has: function(target, prop) {return false;}
});
'a' in p // TypeError is thrown

has方法拦截的是HasProperty操作,而不是HasOwnProperty操作,即has方法不判断一个属性是对象自身的属性,还是继承的属性。可以看作是针对 in 操作的钩子。

5. construct

用于拦截new命令

var handler = {construct (target, args, newTarget) {return new target(...args);}
};

接受三个参数,目标对象,构造函数的参数对象,new命令作用的构造函数。

construct方法返回的必须是一个对象,否则会报错。

6. deletePrototype

拦截delete操作,如果这个方法抛出错误或者返回false,当前属性就无法被delete命令删除。

var handler = {deleteProperty (target, key) {invariant(key, 'delete');delete target[key];return true;}
};
function invariant (key, action) {if (key[0] === '_') {throw new Error(`Invalid attempt to ${action} private "${key}" property`);}
}
var target = { _prop: 'foo' };
var proxy = new Proxy(target, handler);
delete proxy._prop
// Error: Invalid attempt to delete private "_prop" property

deleteProperty方法拦截了delete操作符,删除第一个字符为下划线的属性会报错。
目标对象自身的不可配置(configurable)的属性,不能被deleteProperty方法删除,否则报错。

7. defineProperty

拦截了Object.defineProperty操作。

var handler = {defineProperty (target, key, descriptor) {return false;}
};
var proxy = new Proxy({}, handler);
proxy.a = 'a' // 不会生效

8. getOwnPropertyDescriptor

拦截Object.getOwnPropertyDescriptor(),返回一个属性描述对象或者undefined

var handler = {getOwnPropertyDescriptor (target, key) {return Object.getOwnPropertyDescriptor(target, key);}
};
var target = {a: 'a' };
var proxy = new Proxy(target, handler);
Object.getOwnPropertyDescriptor(proxy, 'a')
// { value: 'a', writable: true, enumerable: true, configurable: true }

9. getPrototypeOf

用来拦截获取对象原型。

var proto = {};
var p = new Proxy({}, {getPrototypeOf(target) {return proto;}
});
Object.getPrototypeOf(p) === proto // true

10. isExtensible

拦截Object.isExtensible操作。

var p = new Proxy({}, {isExtensible: function(target) {console.log("called");return true;}
});
Object.isExtensible(p)
// "called"
// true

11. ownKeys

拦截对象自身属性的读取操作。

let target = {a: 1,b: 2,c: 3
};
let handler = {ownKeys(target) {return ['a'];}
};
let proxy = new Proxy(target, handler);
Object.keys(proxy)
// [ 'a' ]

12. preventExtensions

拦截Object.preventExtensions()

var proxy = new Proxy({}, {preventExtensions: function(target) {return true;}
});
Object.preventExtensions(proxy)
// Uncaught TypeError: 'preventExtensions' on proxy: trap returned truish but the proxy target is extensible

13. setPrototypeOf

用来拦截Object.setPrototypeOf方法。

var handler = {setPrototypeOf (target, proto) {throw new Error('Changing the prototype is forbidden');}
};
var proto = {};
var target = function () {};
var proxy = new Proxy(target, handler);
Object.setPrototypeOf(proxy, proto);
// Error: Changing the prototype is forbidden

只要修改target的原型对象,就会报错。

proxyObject.defineProperty具有的优势

了解Proxy的基本用法过后接下来我们再深入探讨一下相比于Object.defineProperty, Proxy到底有哪些优势。

首先最明显的优势就是在于Proxy要更为强大一些,那这个强大具体体现在Object.defineProperty只能监听到对象属性的读取或者是写入,而Proxy除读写外还可以监听对象中属性的删除,对对象当中方法的调用等等。

这里我们为obj对象定义一个Proxy对象,在Proxy对象的处理对象中的外的添加一个deleteProperty的代理方法,这个方法会在外部对当前这个代理对象进行delete操作时会自动执行。

这个方法同样接收两个参数,分别是代理目标对象和所要删除的这个属性的名称。

const obj = {a: 1
}
const proxy = new Proxy(obj, {deleteProperty(target, propKey) {delete target[propKey]}
})delete proxy.a

vue2.x的响应式并不具备这样的处理,而是需要一个$delete方法来解决。

export default {data() {return {obj: {a: 1}}},methods: {deleteA() {delete this.obj.a;    // a属性确实删除了,但是没有触发对应的更新// this.$delete(this.obj, 'a')  能删除a,并触发更新}}
}

比如上方的使用delete直接删除a,并不会触发更新,这是因为Object.defineProperty只能监听到对象属性的读取或者是写入,没办法监听删除。我们顺便来看看vue2.x的解决办法:

function del(target: any[] | object, key: any) {// ...if (isArray(target) && isValidArrayIndex(key)) {target.splice(key, 1)return}const ob = (target as any).__ob__// ...delete target[key]if (!ob) {return}// 获取对象的观察者__ob__,并手动调用更新if (__DEV__) {ob.dep.notify({type: TriggerOpTypes.DELETE,target: target,key})} else {ob.dep.notify()}
}

可以看到 vue 实例的 $delete 处理了一些边界情况,处理了数组和对象的删除,最重要的是,获取了变量上的ob指针指向的观察者实例,并通知依赖dep里的watcher更新。

第二点优势就是对于数组对象进行监视,

通常我们想要监视数组的变化,基本要依靠重写数组方法,这也是vue2.x的实现方式,proxy可以直接监视数组的变化。以往我们想要通过Object.defineProperty去监视数组的操作最常见的方式是重写数组的操作方法,大体的方式就是通过自定义的方法去覆盖掉数组原型对象上的pushshift之类的方法,以此来劫持对应的方法调用的过程。

这里来看如何直接使用Proxy对象来对数组进行监视。这里我们定义一个list数组,然后对这个list数组进行Proxy监视。

在这个Proxy对象的处理对象上我们去添加一个set方法,用于监视数据的写入,在这个方法的内部我们打印参数的值,然后再target对象上设置传入的值,最后返回一个true表示写入成功。

这样我们再外部对数组的写入都会被监视到,例如我们这里通过push向数组中添加值。

const arr = []
const proxy = new Proxy(arr, {set(target, propKey, value) {target[propkey] = value;return true}
})

Proxy内部会自动根据push操作推断出来他所处的下标,每次添加或者设置都会定位到对应的下标property

最后相比于Object.defineProperty还有一点优势就是,Proxy是以非入侵的方式监管了对象的读写,那也就是说一个已经定义好的对象我们不需要对对象本身去做任何的操作,就可以监视到他内部成员的读写,而defineProperty的方式就要求我们必须按特定的方式单独去定义对象当中那些被监视的属性。