跳到主要内容

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)。
js
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实现更优雅的校验器​

设置了不合法的值时,控制台将会剖出错误,如果在实际业务中,我们可以给用户做出适当的提醒.

js
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 中真正的私有属性​

js
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 进行拦截,当得到时间字符串之后,可以自动加上时间戳。
js
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函数拦截器
    • 索引也是属性,是可以监听数组元素变化的
js
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

深度监听​

js
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);
}
}
});
}
}
}