JavaScript中的代理(Proxy)与反射(Reflect)

857 阅读7分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第4天,点击查看活动详情

相信不少人学习Vue3的时候被各种新增的API吓唬住,比如Refreactive,其中我认为最核心的就是尤雨溪把实现响应式原理的代码重写了:从原来的Object.defineProperty改写成为Proxy方式去实现(当初vue发布的时候是2013年,Proxy还没设计出来呢,Object.defineProperty自然是当时最好的方式)。这篇文章帮你理解Proxy以及讲讲Reflect

代理基础

在红宝书上,介绍代理是目标对象的抽象,目标对象既可以直接被操作,也可以通过代理来操作,但如果直接操作会绕开代理施予的行为

其实理解代理可以跟之前我们所说的Object.defineProperty关联起来,你可以想象成一个加强版的它(当然这么说只是初步帮我们理解,实际上这么说非常不严谨,如果单单是考虑内部实现的方式来说是可以这么关联的)。按照我的理解来说,代理相当于在你所代理的源对象上罩上一个拦截层,外部对源对象的访问都必须通过这个所谓的拦截层,那么代理的作用就是在该拦截层对外部的访问进行设置。在代理上进行改动会直接反映在源对象上,源对象自身的改变也会牵动着代理的改变,如果看过动漫JOJO的可以理解成替身与替身使者的感觉。下面举个例子来帮助我们理解:

const person = {
    name : "路飞",
    age : 18
}
let proxy = new Proxy(person,{ 
    get:function(target,property){
        return target[property]
    }
})

person.name           // 路飞
proxy.name            // 路飞
// 改变源对象身上的属性
person.name = "乌索普"

person.name          // 乌索普
// 代理的数据也会发生改变
proxy.name           // 乌索普

// 通过代理改变数据
proxy.name = "娜美"
// 源对象身上的数据也会发生改变
person.name         // 娜美
proxy.name          // 娜美

通过代理person对象我们生成了代理对象proxy,使用代理对象可以轻松访问到person里的属性与方法

Proxy的使用

讲完代理的基本概念,我们接下来介绍如何定义和使用Proxy

语法

const p = new Proxy(target,handler)

Proxy需要通过new操作符创建一个对象的代理

参数

  • target 需要代理的源对象,可以是任意类型的对象,可以赋空对象{}
  • handler 处理程序对象,该参数是一个容器对象,里面用来包含多种捕获器,用来定制拦截行为

捕获器就是在处理程序对象中定义的" 基本操作的拦截器 ",在处理程序对象上可以包含零个或者多个捕获器,每个捕获器对应一种基本操作。每次在代理对象上调用这些基本操作时,代理可以在这些操作传播到源对象之前调用捕获器函数,从而拦截或修改相应的行为。

具体的一些捕获器的方法可以参考阮一峰的ES6入门👉Proxy - ECMAScript 6入门 (ruanyifeng.com)

几乎所有的捕获器都是对标Object里的方法写的,比如getPrototypeOf(target)就是用来拦截原生对象的Object.getPrototypeOf(proxy),并返回一个对象。

可撤销代理

Proxy暴露了revocable()方法,用来撤销代理对象与目标对象的关联。该方法会返回一个对象,结构为{'proxy':proxy,'revoke':revoke} 其中proxy跟我们正常用new生成的对象没有啥区别,只是它可以被撤销。

const target = {
    foo:"bar"
}
const handler = {
    get(){
        return 'intercepted'
    }
}
const {proxy.revoke} = Proxy.revocable(target,handler)

console.log(proxy.foo)    // intercepted
console.log(target.foo)   // bar
// 调用撤销方法
revoke()

console.log(proxy.foo)    // TypeError

this问题

Proxy代理目标对象的情况下,目标对象的this会指向代理,这样可能会引发一些问题:

const wm = new WeakMap()

class Person{
    constructor(uid){
        wm.set(this,uid)
    }
    get uid(){
        return wm.get(this)
    }
    set uid(newUid){
        wm.set(this,newUid)
    }
}

const p = new Person(123)
p.uid   // 123
const proxy = new Proxy(p,{})
proxy.uid  // undefined

这里显示undefined主要原因就是我们的WeakMap里是按照this作为键的,每一个键都对应一个Person的实例对象,当我们使用proxy代理对象的时候,this指向我们的代理,它会去代理对象里去找uid,那必然是找不到的。

解决办法也很简单,就是让原来用对象实例作为WeakMap的键改为用代理对象作为它的键。

const proxyPerson = new Proxy(Person,{})
cosnt p = new proxyPerson(456)
p.uid   // 456

反射基础

其实理解反射(Reflect)的时候,我们不应该从他的定义出发,我认为我们需要从它设计的意义着手。相比之前通过使用Object动态获取和操作自身内容,使用反射会更加合理,那么这个合理正是反射的意义。

从使用场景来看,反射和Object几乎没有区别,比如我们可以通过反射去获取对象上的属性和方法:

const person = {
    name:"小智",
    age:18
}
Reflect.get(person,"name")   // 小智

上面的例子可以发现,通过反射的方式让操作对象从原来的命令式转变成函数式,函数式的好处就太多了,可以简单看看这篇文章👉说说你对函数式编程的理解?优缺点? - 知乎 (zhihu.com)

在红宝书上列举了使用反射的场景

(1) 反射API并不限于捕获处理程序😅这个稍后再说

(2) 大多数反射API在Object上都有对应的方法,通常Object适用于通用程序,而反射使用于细粒度的对象控制于操作

反射的API其实可以参考代理👉Reflect - ECMAScript 6入门 (ruanyifeng.com)

代理+反射

一般我们在用代理的时候会和反射一起搭配使用,回到上面的例子:

const person = { 
    name : "路飞", 
    age : 18 
} 
let proxy = new Proxy(person,{ 
    get:function(target,property){ 
        // 原先写法
        // return target[property]
        // 使用反射写法
        Reflect.get(target,property)
    } 
})

其实可以不使用反射,但不是所有捕获器的方法都像get()那样简单,通过手写所有代码完成逻辑显然是不现实的,处理程序对象中所有可以捕获捕获的方法都有对应的反射。反射API为开发者准备好了样板代码,我们可以用最少的代码修改捕获的方法。

反射使得代码更加合理

开发程序时,如果发生问题,我们更希望知道哪里发生问题,而不是抛出错误,一行一行去调试:

const o ={}
try{
    Object.defineProperty(o,'foo','bar')
}catch(e){
    console.log('error')
}

使用反射避免了大量的去写try/catch语句,如果发生问题,会返回false,这样会让我们的代码更加合理化

Vue响应式式原理实现

最后还是想说一下vue这块,之前看视频学的vue老师着重强调了实现原理,我就简写一个vue的响应式原理

vue2

vue2的话根本上就是用Object.defineProperty()来实现的

const person = {
    name:"神秘人",
    age:20
}
// 定义一个空对象
const p = {}

Object.defineProperty(p,"name",{
    get(){
        console.log('有人访问了name属性')
        return person.name
    },
    set(newName){
        console.log('有人修改了name属性')
        person.name=newName
    }
})
p.name   // 神秘人
         // 有人访问了name属性
p.name = "xyz" //  有人修改了name属性
person.name    // xyz

这样一个简陋的响应式原理就完成了,但是它有个很明显的缺陷,他只能捕获到获得属性与修改属性的行为,对于添加和删除捕获不到

vue3

vue3通过代理反射的方式解决了上面的问题

const person = {
    name:"神秘人",
    age:20
}

const proxy = new Proxy(person,{
    get(target,property){
        console.log(`有人访问了${property}属性`)
        return Reflect.get(target,property)
    },
    set(target,property,value){
        console.log(`有人修改了${property}属性`)    
        let result = Reflect.set(target,property,value)
        return result
    },
    deleteProperty(target,property){
        console.log(`有人删除了${property}属性`)
        return Reflect.deleteProperty(target,property)
    }
})
proxy.name             // 神秘人
                       // 有人访问了name属性
proxy.name = "奇人"    // 有人修改了name属性
porxy.age = "男"       //有人修改了sex属性
delete proxy.name     // 有人删除了name属性
                      // true

通过Proxy解决了Object.defineProperty()捕获不到添加与删除的问题,只不过在Proxy中添加和修改被一同写在了set()方法中了,对于一些特别的捕获形式会有专门的函数API可以使用

最后

代理与反射是ES6中非常重要的更新,它在某种程度上也解决了Object臃肿和不合理性,在以后的发展中Object里的方法会逐渐的移步到Reflect中,从Vue3使用代理反射占据着越来越重要的作用。