Proxy/Reflect
参考: https://www.zhangxinxu.com/wordpress/2021/07/js-proxy-reflect/
- Proxy用于创建一个对象的代理,从而实现基本操作的拦截和自定义(如属性查找、赋值、枚举、函数调用等)
- Reflect反射,它提供拦截 JavaScript 操作的方法
- Reflect对象挂载了13中静态方法,这些方法与 Proxy 的方法相同
- 比如你可以使用 Reflect.apply() 方法来拦截函数的调用
Proxy
Proxy 可以理解成,在目标对象之前架设一层“拦截”,外界对该对象的访问,都必须先通过这层拦截,因此提供了一种机制,可以对外界的访问进行过滤和改写。Proxy在很多场景中都会和Reflect一起使用
proxy的支持的拦截操作的方法,一共13种
- get(target, propKey, receiver):拦截对象属性的读取,比如
proxy.foo和proxy['foo']。 - set(target, propKey, value, receiver):拦截对象属性的设置,比如
proxy.foo = v或proxy['foo'] = v,返回一个布尔值。 - has(target, propKey):拦截
propKey in proxy的操作,返回一个布尔值。 - deleteProperty(target, propKey):拦截
delete proxy[propKey]的操作,返回一个布尔值。 - ownKeys(target):拦截
Object.getOwnPropertyNames(proxy)、Object.getOwnPropertySymbols(proxy)、Object.keys(proxy)、for...in循环,返回一个数组。该方法返回目标对象所有自身的属性的属性名,而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),返回一个布尔值。 - 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)。
const proxy = new Proxy(obj, {
/**
* 当访问属性的时候,才会触发get方法
* target:要拦截的目标对象
* key:目标对象的属性
* receiver: 指向当前Proxy代理对象 或者 继承Proxy的对象
*/
get(target, key, receiver) {
// console.log('receiver', receiver === proxy); // true
// return target[key];
return Reflect.get(target, key, receiver);
},
set(target, key, value, receriver) {
// target[key] = value;
/**
* Reflect.set一旦传入receiver,就会将属性赋值到receiver上面(即obj),导致触发 defineProperty拦截。如果Reflect.set没有传入receiver,那么就不会触发defineProperty拦截
*/
return Reflect.set(target, key, value, receriver);
},
defineProperty(target) {
console.log('target', target);
},
});
proxy应用场景
利用proxy实现更优雅的校验器
设置了不合法的值时,控制台将会剖出错误,如果在实际业务中,我们可以给用户做出适当的提醒.
const formData = {
name: 'xu',
age: 120,
label: ['react', 'vue', 'node', 'javascript'],
}
// 校验器
const validators = {
name(v) {
// 检验name是否为字符串并且长度是否大于3
return typeof v === 'string' && v.length > 3
},
age(v) {
// 检验age是否为数值
return typeof v === 'number'
},
label(v) {
// 检验label是否为数组并且长度是否大于0
return Array.isArray(v) && v.length > 0
},
}
// 代理校验对象
function proxyValidator(target, validator) {
return new Proxy(target, {
set(target, propKey, value, receiver) {
if (target.hasOwnProperty(propKey)) {
let valid = validator[propKey]
if (!!valid(value)) {
return Reflect.set(target, propKey, value, receiver)
} else {
// 一些其他错误业务...
throw Error(`值验证错误${propKey}:${value}`)
}
}
},
})
}
let formObj = proxyValidator(formData, validators)
formObj.name = '44'
实现JavaScript 中真正的私有属性
var api = {
_apiKey: '123abc456def',
/* mock methods that use this._apiKey */
getUsers: function(){ },
getUser: function(userId){ },
setUser: function(userId, config){ }
};
// Add other restricted properties to this array
const RESTRICTED = ['_apiKey'];
api = new Proxy(api, {
get(target, key, proxy) {
if(RESTRICTED.indexOf(key) > -1) {
throw Error(`${key} is restricted. Please see api documentation for further info.`);
}
return Reflect.get(target, key, proxy);
},
set(target, key, value, proxy) {
if(RESTRICTED.indexOf(key) > -1) {
throw Error(`${key} is restricted. Please see api documentation for further info.`);
}
return Reflect.get(target, key, value, proxy);
}
});
// throws an error
console.log(api._apiKey);
// throws an error
api._apiKey = '987654321';
数据格式化
- 比如有一个需求,需要传时间戳给到后端,但是前端拿到的是一个时间字符串,这个也可以用
proxy进行拦截,当得到时间字符串之后,可以自动加上时间戳。
let ecDate = {
set (target, key, value, receiver) {
if(key === 'date'){
Reflect.set(target, 'timeStamp', +new Date(value), receiver)
}
return Reflect.set(target, key, value, receiver)
}
}
let obj=new Proxy({date:''},ecDate)
为什么Proxy一定要配合Reflect使用
- Relfect 中的 receiver 可以修改属性访问中的 this 指向为传入的 receiver 对象
- receiver 对象 指向当前Proxy代理对象 或者 继承Proxy的对象
- 所以配合Relect可以改变访问中的this指向Proxy代理对象
若需要在Proxy内部调用对象的默认行为,建议使用Reflect
-
只要
Proxy对象具有的代理方法,Reflect对象全部具有 -
修改某些
Object方法的返回结果,让其变得更合理 -
让
Object操作都变成函数行为
Reflect设计的目的
- 1、将显属于语言内部的方法放到Reflect对象上 (如Object.defineProperty)
- 2、修改某些
Object方法的返回结果,让其变得更合理Reflect的出现是为了让Object的操作更加规范
- 3、可以解决proxy代理对象this的指向问题
- vue3源码中响应式原理,就是使用了Reflect来解决proxy的this指向问题
proxy为什么不可被polyfill?
- proxy无法通过降级兼容处理,proxy暴露了对象基本操作,可以拦截对象的任何操作,而这个是无法被模拟实现的
可以被 Polyfill 的有:
-
如class可以用function模拟
-
如promise可以用callback模拟
不可 Polyfill 的有:
- proxy的功能用Object.defineProperty无法模拟!
- proxy的功能 无法用其他语法来模拟
- proxy支持拦截的方法 有13种
因此vue3不支持ie, proxy不支持ie
Object.defineProperty理解
缺点
-
Object.defindProperty监听长列表数组会有性能问题,与用户体验不成正比
-
为了解决这个问题, vue 2内部把原生数组的方法push、pop、splice等等7个方法进行重写
-
重新定义数组的原型,来重写数组方法, 实现原理
- 1、创建新对象,改变原型指向,保证重写数组的方法不会影响原有的原型
-
-
只能劫持对象的属性,因此需要对对象的每个属性进行递归遍历
-
数据量复杂时 性能低
Object.defineProperty可以监听数组长度变化吗
- Object.defineProperty可以监听数组变化,监听数组索引,也是可以触发set函数拦截器
- 索引也是属性,是可以监听数组元素变化的
const arr = [1,2,3]
arr.forEach((item,index)=>{
Object.defineProperty(arr,index,{
set:(val)=>{
console.log('set');
item = val
},
get:()=>{
console.log('get');
return item
}
})
})
arr[1] // 触发get
arr[1] = '4' // 触发set
深度监听
function deepWatch(obj, callback) {
for (const key in obj) {
if (obj.hasOwnProperty(key)) {
let val = obj[key];
// 递归监听子对象属性
if (typeof val === 'object') {
deepWatch(val, callback);
}
Object.defineProperty(obj, key, {
get: function () {
return val;
},
set: function (newVal) {
if (newVal !== val) {
val = newVal;
callback(key, newVal);
}
}
});
}
}
}