zl程序教程

您现在的位置是:首页 >  其他

当前栏目

重学ES6之代理Proxy和反射Reflect

2023-03-14 22:34:58 时间

Proxy代理,Reflect反射。

调用new Proxy()可以创建代替其他目标(target)对象的代理,它虚拟化了目标,所以两者看起来功能一致。 代理可以拦截JavaScript引擎内部目标的底层对象操作,这些底层操作被拦截后会触发相应特定操作的陷阱函数。(钩子函数Hook) 反射API以Refelect对象的形式出现,对象中方法的默认特性与相同的底层操作一致,而代理可以覆写这些操作,每个代理陷阱对应一个命名和参数都相同的Refelect方法。

Tips

无论是Object.definProperty()还是new Proxy()对于数组的代理,数字的原本方法比如push(),pop(),shift()等这些方法是可以不被拦截的,所以这也就Vue底层对于数组的监听在原型上重写了这些方法。(虽然Vue不支持对于数组下标形式的修改,但是这两种方式是支持数组下标的拦截的。) 为什么不用数组的拦截并不是不支持而是->参见github上尤大的回答。

![截屏2020-08-10 下午8.55.31.png]

ES5中拦截对象的属性

let obj = {}
let newVal = ""
Object.defineProperty(obj,'name',{
    get() {
        return newVal
    }
    set(val) {
        newVal = val
    }
})
复制代码

ES6使用New Proxy进行拦截(常见拦截)

==get==

  • get(target,key,receiver) 拦截 -> 默认Reflect.get(target,key,receiver)

target源对象(被代理对象),key获取的属性值,receiver操作发生的对象(代理对象)

let obj = {}
const p = new Proxy(obj,{
    
})
p.name = 'imooc'
console.log(obj.name) // imooc
// 上面代码没有进行任何配置代理拦截 所以对于代理p的才会原封不动的到obj上


// 增加一些拦截的钩子
// 注意这个例子是用in检查receiver(代理)而非target,
// 是因为防止receiver是否含有has陷阱,
// 如果有has陷阱就可以对in进行一层拦截。
// 而源对象target可能会忽略in操作符的has陷阱从而引发错误。
cosnt array = [7,8,9]
const p = new Proxy(arr, {
    get(target,props,receiver) {
        // target 表示源对象(被代理的对象)
        // props 表示读取的属性key值
        // receiver表示操作发生的对象(通常是代理)
        if(!(props in receiver)) {
            throw new TypeError("key值不存在")
        }
        // Reflect反射API以对象的形式出现,对象中方法的默认特性与相同的底层操作一致。
        // 也就是return Refelct API就是默认的get值。
        return Reflect.get(target,props,reveiver)
    }
})
复制代码

==set==

  • set(target,key,value,receiver)拦截 -> 默认 Reflect.set(target,key,value,receiver)

target源对象(用于接收属性的对象),key要写入的属性键,value被写入属性的值,receiver操作发生的对象(代理对象)。

let arr = new Proxy(arr,{
    set(target,key,value,receiver) {
        if(typeof key !== 'number') {
            throw new TypeError("属性必须是数字")
        }
        return Reflect.set(target,key,value,receiver)
    }
})
复制代码

==has==

  • has(target,key) 拦截 -> 默认 Refelect.has(target,key) (对于in拦截)

target(源对象),key(in操作符前的属性key值)

let range = {
    name:"target",
    vlaue:42
}
let proxy = new Proxy(range, {
    has(target,key) {
        if(key === 'value') {
            return false // 表示碰到key为value的返回false
        }else {
            return Reflect.has(target,key)
        }
    }
})
复制代码

==ownKeys==

  • ownKeys(target) 拦截 -> Refelect.ownKeys(target)

target源对象。(ownKeys对于Object.keys(),Object.getOwnPropertyNames(),Object,getOwnPropertySymbols(),for in进行拦截) ownKeys陷阱唯一接收的参数事操作的目标,返回值必须是一个数组或者类数组对象,否则就会抛出错误。当调用Object.keys(),Object.getOwnPropertyNames(),Object.getOwnPropertySymbols()或者Object.assign()以及for in方法的时候,可以用ownKeys陷阱过滤不想使用的属性键。

let userInfo = {
    username:"wanghaoyu",
    age:23,
    _password:"***" // 下划线开头的***我并不想被Object.keys(),forin等取到
}

// 这个例子使用了一个ownKeys陷阱,它首先调用Reflect.ownKeys()获取目标的默认key列表
// 接下来用filter()方法过滤掉以下划线自负开始的字符串。然后将过滤后的数组元素添加到proxy的方法返回上
user = new Proxy(userInfo,{
    ownKeys(target) {
    // 返回不以_开头的属性数组
        return Reflect.ownKeys(tarrget).filter(i => !i.startsWith("_"))
    }
})
Object.keys(userInof) // [username,age]
复制代码

==deleteProperty==

  • deleteProperty(target,key)陷阱拦截删除 -> Reflect.deleteProperty(target,key)

target源对象,key需要删除的key值。 ES5中删除属性delete属性操作符,成功返回true,不成功返回false,严格模式下会报错。配置Object.defineProperty(obj,'xx',{ configurable:false })设置属性不可以配置也就不可以删除了。 ES6中可以使用代理Proxy代理deleteProperty()来改变这个行为。

let user = {
    name:"wanghaoyu",
    age:18,
    _password:"***"
}
// 我希望下划线开头的_password属性不可以删除
user = new Proxy(user,{
    deleteProperty(target,key) {
        if(key.startsWith("_")) {
            return false
        }else {
            return Reflect.deleteProperty(target,key)
        }
    }
})
复制代码

函数有两个内部方法[call]和[construct],apply陷阱和constructor陷阱可以复写这些内部方法,若使用new操作符调用函数,则执行construct方法。则会执行construct陷阱。 若不用则调用call方法。执行会执行apply陷阱(以及apply,call调用函数都会进入apply陷阱)。

小Tips:注意apply和construct陷阱的写法是x:()=> {} 函数写法,其他属性陷阱是a() {} 写法。

==apply==

  • apply(target,thisArg,argumentsList)陷阱对于函数的拦截。 -> Reflect.apply(target,this.Arg,argumentsList)

target被执行的元素(被代理的函数),thisArg函数被调用时内部this的值,argumentsList传递给函数的参数数组。

// 注意形参上的...args并不是扩展,是rest参数
// 将传入的参数合成成为一个数组Array
let sum = (...args) => {
    let num = 0
    args.forEach(i => {
        num += i
    })
    return num
}

// proxy apply
let proxy = new Proxy(sum,{
    apply:(target,this,args) => {
        return Reflect.apply(target,this,args) *2
    }
})

console.log(sum(10,16)) // 26
console.log(proxy(10,12)) // 52
复制代码

==constructor==

  • constructor(target,argumentList,newTarget)陷阱 -> Reflect.constructor(target,argumentList,newTarget)
  • target被代理的class类,argumentList参数列表,newTarget指的是创造实例的时候new命令作用的那个构造函数。
let user = class User {
    
}

let proxy = new Proxy(user,{
    construct: (target,arguments,newTarget) => {
     return Reflect.constructd(target,arguments,newTarget)   
    }
})

复制代码

Vue

Vue响应式原理在2.X中使用Object.defineProperyt()进行响应式拦截,而Vue3已经使用了Proxy进行属性的劫持了。