Proxy是ES6的语法。对于ES6的箭头函数、变量的解构赋值,我们都能耳熟能详,熟练使用。对于Proxy 这样的特性却很少用到,一方面考虑到兼容性的问题(babel已经解决了),另一方面确实这些特性不那么容易使用,也就是使用场景不多。
Proxy主要是用来修改某些操作的默认行为,从这个角度来讲,你可以把他理解成一个拦截器。想要访问对象,都要经过这层拦截。那么我们就可以在这层拦截上做各种操作了。比如你设置一个对象的值的时候,对对象的值进行校验等。
Proxy 支持的拦截操作一共 13 种:
- get(target, propKey, receiver):拦截对象属性的读取,比如proxy.foo和proxy['foo']。如果一个属性不可配置(configurable)和不可写(writable),则该属性不能被代理,通过 Proxy 对象访问该属性会报错。
- set(target, propKey, value, receiver):set方法的第四个参数receiver,总是返回this关键字所指向的那个对象,即proxy实例本身。代表拦截对象属性的设置,比如proxy.foo = v或proxy['foo'] = v,返回一个布尔值。
- has(target, propKey):拦截propKey in proxy的操作,返回一个布尔值。值得注意的是,has方法拦截的是HasProperty操作,而不是HasOwnProperty操作,即has方法不判断一个属性是对象自身的属性,还是继承的属性。
- deleteProperty(target, propKey):拦截delete proxy[propKey]的操作,返回一个布尔值。
- ownKeys(target):拦截Object.getOwnPropertyNames(proxy)、Object.getOwnPropertySymbols(proxy)、Object.keys(proxy),返回一个数组。该方法返回目标对象所有自身的属性的属性名,而Object.keys()的返回结果仅包括目标对象自身的可遍历属性。
- getOwnPropertyDescriptor(target, propKey):拦截Object.getOwnPropertyDescriptor(proxy, propKey),返回属性的描述对象。
- defineProperty(target, propKey, propDesc):拦截Object.defineProperty(proxy, propKey, propDesc)、Object.defineProperties(proxy, propDescs),返回一个布尔值。
- preventExtensions(target):拦截Object.preventExtensions(proxy),返回一个布尔值。
- getPrototypeOf(target):拦截Object.getPrototypeOf(proxy),返回一个对象。
- isExtensible(target):拦截Object.isExtensible(proxy),返回一个布尔值。这个方法有一个强限制,它的返回值必须与目标对象的isExtensible属性保持一致,否则就会抛出错误。
- setPrototypeOf(target, proto):拦截Object.setPrototypeOf(proxy, proto),返回一个布尔值。如果目标对象是函数,那么还有两种额外操作可以拦截。
- apply(target, object, args):拦截 Proxy 实例作为函数调用的操作,比如proxy(...args)、proxy.call(object, ...args)、proxy.apply(...)。
- construct(target, args):拦截 Proxy 实例作为构造函数调用的操作,比如new proxy(...args)。
通过proxy可以做什么呢?
1.用来拦截和监听对对象属性的各种操作,可以在各种操作之前做校验或者打日志等。这个很简单了。
2.中断操作
目标对象不允许直接访问,必须通过代理访问,一旦访问结束,就收回代理权,不允许再次访问。
let target = {};
let handler = {};
let {proxy, revoke} = Proxy.revocable(target, handler);
proxy.foo = 123;
proxy.foo // 123
revoke();
proxy.foo // TypeError: Revoked
3.可以通过Proxy实现是对象的私有变量(真正的私有)
// filter参数作为一个过滤函数出入
function priavateProp(obj, filter){
const handler = {
get(obj, prop) {
if(!filter(prop)){
let val = Reflect.get(obj, prop)
if(typeof val === 'function'){
val = val.bind(obj)
}
return val
}
},
set(obj, prop, val) {
if(filter(prop)){
throw new Error(`cannot set property ${prop}`)
}
return Reflect.set(obj, prop, val)
},
has(obj, prop) {
return filter(prop) ? false : Reflect.has(obj, prop)
},
ownKeys(obj) {
return Reflect.ownKeys(obj).filter( prop => !filter(prop))
}
}
return new Proxy(obj, handler)
}
// 私有属性过滤器
// 规则:以 _ 为开头的属性都是私有属性
function filter(prop){
return prop.indexOf('_') === 0
}
const o = {
_private: 'private property',
name: 'public name',
say(){
// 内部访问私有属性
console.log(this._private)
}
}
const p = priavateProp(o, filter)
console.log(p) // Proxy
p._private // undefined
JSON.stringify(p) // "{"name":"public name"}"
// 只能内部访问私有属性
p.say() // private property
console.log('_private' in p) // false
// 不能遍历到私有属性
Object.keys(p) // ["name", "say"]
// 私有属性不能赋值
p._private = '000' // Uncaught Error: cannot set property _private
4.扩充操作符
使用 Proxy 可是实现操作符的重载,但也只能对 in of delete new 这几个实现重载
我们劫持 in 操作符来实现 Array.includes 检查值是否存在数组中
function arrIn(arr){
const handler = {
has(arr, val) {
return arr.includes(val)
}
}
return new Proxy(arr, handler)
}
const arr = arrIn(['a', 'b', 'c'])
'a' in arr // true
1 in arr // false
5.追踪数组和对象的变动。据说下一版本的VUE就是通过这种方式来进行数据绑定的。
function trackChange(target, fn){
const handler = {
set(target, key, value) {
const oldVal = target[key]
target[key] = value;
fn(target, key, oldVal, value)
},
deleteProperty(target, key) {
const oldVal = target[key]
delete target[key]
fn(target, key, oldVal, undefined)
return true;
}
}
return new Proxy(target, handler)
}
const obj = trackChange({}, (target, key, oldVal, newVal) => {
console.log(`obj.${key} value from ${oldVal} to ${newVal}`)
})
obj.a = 'a111' // obj.a value from undefined to a111
obj.a = 'axxxxx' // obj.a value from a111 to axxxxx
delete obj.b // obj.b value from undefined to undefined
obj.c = 'c1' // obj.c value from undefined to c1
const arr = trackChange([1, 2, 3, 4, 5], (target, key, oldVal, newVal) => {
let val = isNaN(parseInt(key)) ? `.${key}` : `[${key}]`
const sum = arr.reduce( (p,n) => p + n)
console.log(`arr${val} value from ${oldVal} to ${newVal}`)
console.log(`sum [${arr}] is ${sum}`)
})
arr[4] = 0
// arr[4] value from 5 to 0
// sum [1,2,3,4,0] is 10
delete arr[3]
// arr[3] value from 4 to undefined
// sum [1,2,3,,0] is 6
arr.length = 2
// arr.length value from 5 to 2
// sum [1,2] is 3
里面有些疑点在此披露下:
拿这个deleteProperty(target, propKey)来说,我查了很多资料上说,如果这个方法抛出错误或者返回false,当前属性就无法被delete命令删除。(暂时未找到标准上是怎么解释的)
var handler = {
deleteProperty (target, key) {
console.log('delete '+ key)
return true
},
};
function invariant (key, action) {
if (key[0] === '_') {
return false;
}
}
var target = { _prop: 'foo' };
var proxy = new Proxy(target, handler);
delete proxy._prop
console.log(proxy._prop)
console.log(target._prop)
上面的代码返回true,并没有删除。其实如果你在这个函数里面执行了删除操作,返回什么并不重要。也就是说这个函数返回值是true,或者false,并不能影响删除的结果。除非你抛出来错误,那这个代码就执行不了了,直接报错了。试验了defineProperty,也是这样的。这些拦截函数,返回结果是不重要的,重要的是你在拦截相应的操作的时候,你做了什么操作,比如你拦截的是删除属性,那你必须要有删除属性这个操作,你要是给这个属性赋值,那肯定达不到效果。这一点要搞清楚。
但是如果代理的是个数组,那他们return的值就必须得是true。
var handler = {
set(target,key,value,proxy){
var oldVal = target[key];
var newVal = value;
target[key] = value
console.log(`arr ${key} changed from ${oldVal} to ${newVal}`)
},
deleteProperty (target, key) {
console.log('delete '+ key)
delete target[key];
},
};
var arr = new Proxy([1,2,3], handler);
arr.push(2);//'set' on proxy: trap returned falsish for property '3'
arr.pop();//'deleteProperty' on proxy: trap returned falsish for property '2'
这就是set和deleteProperty不返回结果,爆出来的问题。每个函数中返回true,返回结果就是
arr 3 changed from undefined to 2
arr length changed from 4 to 4
delete 3
arr length changed from 4 to 3
所以不管怎么样,对于这类返回boolean类型的方法,我们一定要记得返回值。
还有一种规范的写法,我们一般都是通过Reflect进行拦截操作,然后将其结果返回。
var handler = {
set(target,key,value,proxy){
var oldVal = target[key];
var newVal = value;
console.log(`arr ${key} changed from ${oldVal} to ${newVal}`)
return Reflect.set(target,key,value,proxy);
},
deleteProperty (target, key) {
console.log('delete '+ key)
return Reflect.deleteProperty(target,key)
},
};
var arr = new Proxy([1,2,3], handler);
arr.push(2);
arr.pop();
那什么是Reflect呢?
Reflect对象与Proxy对象一样,也是ES6为了操作对象而提供的新API。Reflect对象的设计目的有这样几个。
- 将Object对象的一些明显属于语言内部的方法(比如Object.defineProperty),放到Reflect对象上。现阶段,某些方法同时在Object和Reflect对象上部署,未来的新方法将只部署在Reflect对象上。
- 修改某些Object方法的返回结果,让其变得更合理。比如,Object.defineProperty(obj, name, desc)在无法定义属性时,会抛出一个错误,而Reflect.defineProperty(obj, name, desc)则会返回false。
- 让Object操作都变成函数行为。某些Object操作是命令式,比如name in obj和delete obj[name],而Reflect.has(obj, name)和Reflect.deleteProperty(obj, name)让它们变成了函数行为。
- Reflect对象的方法与Proxy对象的方法一一对应,只要是Proxy对象的方法,就能在Reflect对象上找到对应的方法。这就让Proxy对象可以方便地调用对应的Reflect方法,完成默认行为,作为修改行为的基础。也就是说,不管Proxy怎么修改默认行为,你总可以在Reflect上获取默认行为。
它拥有的API和Proxy一一对应。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。