JavaScript ES6 Proxy-Reflect 详解

164 阅读8分钟

JavaScript ES6 Proxy-Reflect 详解

首先这两个部分的话,和后续的 vue3 的响应式原理是息息相关的

Proxy 的使用

监听对象的属性操作

这个操作的话一般是含有增删改查的,我们现在是可以通过一些操作来实现我们的监听对象中属性的操作了

这个就是我们后面学习 vue3 响应式原理的重要的一个知识点, vue3 的底层源码的响应式就是通过的是这个 Proxy 来实现的

响应式: 当一个属性或者对象发生了变化的时候,我们的就会自动的调用某个函数执行,这就是响应式的浅显概念,表观呈现

以前的操作使用的是我们的 Object.defineProperty 里面设置我们的 getter 函数和 seeter 函数来实现的监听的,当即手搓

let obj = {}
​
// 开始设置一个属性,并且实现监听
Object.defineProperty(obj, "name", {
    // 设置 getter 函数
    get: function() {
        return obj.name
    },
​
    // 设置 setter 函数
    set: function(value) {
        name = value
        console.log(name)
    }
})
​
obj.name = "John"  // John
obj.name = "76433"  // 76433
let obj = {}
​
Object.defineProperties(obj, {
    name: {
        get: function () {
            return obj.name
        },
        set: function (value) {
            console.log(value)
        }
    },
​
    age: {
        get: function () {
            return obj.age
        },
        set: function (value) {
            console.log(value)
        }
    }
})
​
obj.age = 42  // 42
obj.name = "76433"  // 76433
obj.name = null  // null
  • 上面实现监听的模式就是我们的 vue2 的响应式原理的底层实现的原理,使用的就是我们的属性描述符

    • let obj = {
          name: "76433",
          age: 18
      }
      const keys = Object.keys(obj)
      for (const key in keys){
          let value = obj[key]
      ​
          Object.defineProperty(obj, key, {
              get: function(){
                  return value
              },
              set: function(new_value) {
                  value = new_value
                  console.log(new_value)
              },
          })
      }
      
    • 但是这种方法实现的监听只能够实现监听我们对象属性的设置和修改的操作,但是那些新增和删除属性是无法实现监听的
    • 这个时候为了优化这种监听就出现了另一个可以用来实现监听的方法,就是我们的 Proxy 代理
  • 但是 vue3 的话响应式原理就是我们的 Proxy 了,那么 proxy 该如何使用呐???

    • 如果我们的使用了这个 Proxy,我们后续进行操作的就是我们创建出来的代理对象,就不是对原本的对象实现监听了

Proxy 的简单使用

Proxy 的基本原理
  • 首先的是这个对象是帮助我们的创建出一个代理对象
  • 如果说我们想要实现监听一个对象的一系列的操作的话,这个时候就可以使用代理,先创建出一个代理对象
  • 然后对这个原对象进行一系列的操作,都是通过间接性的处理代理对象来完成的,通过这个代理对象就可以实现对原对象的一些
  • 操作实现监听
Proxy 的实现步骤
  • 首先需要进行 new Proxy() ,并且传入一个需要进行监听的对象以及一个处理对象,这个就是 handler

    • const proxy_obj = new Proxy(target, handler)
  • 然后我们的后续的操作就是直接对 Proxy 进行一系列的操作,就不是对原本的对象进行操作了,同时对这个代理对象进行的操作

    • 都是由我们的 handler 函数来进行监听的
    • 这些操作含有: 增删改查(熟悉的四字珍言,哈哈哈)

Proxy 的set和get捕获器

这个捕获器具的话就是在我们的 handler 函数中设置的捕获器,实现对相应操作的一种监听操作

const obj = {
    name: "76433",
    age: 18,
    info: {
        message: "hello front-end"
    }
}
​
// 1.根据原对象创建代理对象
const proxy_obj = new Proxy(obj, {
    /**
     * get 捕获器含有三个参数
     * @param target 原本的对象
     * @param key 变动的属性
     * @param receiver 代理对象
     * @returns {*}
     */
    get: function(target, key, receiver) {
        return target[key]
    },
​
    /**
     * set 捕获器含有四个参数
     * @param target 原本的对象
     * @param key 属性
     * @param value 需要进行设置的值
     * @param receiver 代理对象
     */
    set: function(target, key, value, receiver) {
        console.log(`${target[key]}已经修改为 ${value}`)
        target[key] = value
    }
})
​
// 2.对原对象的任何操作都转移到代理对象中去
console.log(proxy_obj)  // { name: '76433', age: 18 }
console.log(proxy_obj.info === obj.info)  // true 通过这一步可以了解到,这是一个浅拷贝
proxy_obj.name = "水逆信封"  // 76433已经修改为 水逆信封

Proxy 其他的捕获器

image-20241121023725838.png

const obj = {
    name: "76433",
    age: 18,
    info: {
        message: "hello front-end"
    }
}
​
// 1.根据原对象创建代理对象
const proxy_obj = new Proxy(obj, {
    /**
     * get 捕获器含有三个参数,监听的是我们的属性的获取操作
     * @param target 原本的对象
     * @param key 变动的属性
     * @param receiver 代理对象
     * @returns {*}
     */
    get: function(target, key, receiver) {
        return target[key]
    },
​
    /**
     * set 捕获器含有四个参数,监听的是我们的属性的设置操作
     * @param target 原本的对象
     * @param key 属性
     * @param value 需要进行设置的值
     * @param receiver 代理对象
     */
    set: function(target, key, value, receiver) {
        console.log(`${target[key]}已经修改为 ${value}`)
        target[key] = value
    },
​
    /**
     * 实现的是监听我们的删除的操作
     * @param target
     * @param key
     */
    deleteProperty: function(target, key) {
        console.log(`实现删除的属性值为: ${target[p]}`)
        delete target[key]
    },
​
    has: function(target, key) {
        return key in target
    }
})
​
// 2.对原对象的任何操作都转移到代理对象中去
console.log(proxy_obj)  // { name: '76433', age: 18 }
console.log(proxy_obj.info === obj.info)  // true 通过这一步可以了解到,这是一个浅拷贝
proxy_obj.name = "水逆信封"  // 76433已经修改为 水逆信封console.log("name" in proxy_obj)  // true
function Foo() {}
​
const proxy_Foo = new Proxy(Foo, {
    apply: function (target, property, receiver) {
        console.log("函数实现了 apply 操作")
    },
​
    construct: function (target, property, receiver, ...args) {
        console.log("函数执行了 new 操作")
        return new target(...args);
    }
})
​
​
proxy_Foo.call("123")
proxy_Foo.apply()
​
new proxy_Foo()

Reflect 的使用

Reflect的使用

他就是我们的一个 ES6 新添加的一个 API,他就是一个对象,就是我们的反射

Reflect 的基本作用

  • 首先该对象的话提供了很多的关于操作 JavaScript 对象的方法,类似于 Object 操作对象的方法
  • 以前学习的基本上所有的 Object 可以用来操作 对象的方法,这个对象上面也是有的
  • Reflect.getProperty(target) 类似于 Object.getProperty(target)

Reflect 和 Object 既然如此相似,为啥会出现呐???

  • 这个的话就是和早期的 JavaScript 的设计模型方面的缺陷有关系了
  • 在早期的 JavaScript 对对象本身的一些操作的话,没有实现规范化,所以说把所有的可以进行操作的 API 全部添加到了该对象上
  • 但是呐 Object 本身也是一种构造函数,所以说这些方法添加到这个的上面也不是十分的合理
  • 为了解决上面的设计模型的缺陷,这个时候就具有了我们的 Reflect 的对象,让进行操作对象的 API 全部添加到了该对象上
  • 另外还可以实现的是,我们在操作我们的 Proxy 的对象的时候,可以实现不操作原对象
  • Reflect 的话就是一个纯对象了,不是一个函数

这里的话拓展一个点:(口语化的解释)【后面有时间再来深度解析】

  • 就是我们的代码的设计的时候,设计的思想是高内聚低耦合高复用
  • 不仅仅需要思考如何让自己的代码可以实现算法的提升,同时还需要考虑的一点是如何提高代码的复用性
  • 这个就是我们后期写代码的时候需要思考的一个点了
  • 算法的提升呢:就是对一个业务逻辑的代码尽可能的减少时间的调度以及内存的占用
  • 复用性的提高呐就是相关于我们的封装的思想,实现后面的可以多次的重复调度
let obj = {
    name: "76433",
    age: 18
}
​
// 开始通过属性描述符来实现我们的给对象中的属性添加权限设置
Object.defineProperty(obj, "name", {
    configurable: false
})
​
// 开始通过操作我们的对象
/**
 * 含有三个参数:
 * 第一个参数: 我们需要进行修改的对象
 * 第二个参数: 需要进行的修改的对象中的属性
 * 返回值是: boolean
 */
Reflect.deleteProperty(obj, "age")
    ? console.log("该属性删除成功...")
    : console.log("该对象不可删除或者已经删除...")

image-20241121034756912.png

Reflect 中常用的方法

  • Reflect.getPrototypeof(target) 实现的是获取指定的对象的隐式原型,类似于 Object.getPrototypeof(target)
  • Reflect.setPrototypeof(target, prototype) 实现的是为目标对象设置隐式原型,类似于 Object.setPrototypeof(target, prototype)
  • 简单的写一写,他和我们的 Proxy 的捕获器可以设置的一些方法是一一对应的
  • Reflect 使用得最多的场景是和 Proxy 的共同使用,共同完成对一个对象的代理
先来一个只使用了 Proxy 的对象对象进行捕获的写法
let obj = {
    name: "76433",
    age: 18
}
​
const proxy_obj = new Proxy(obj, {
    // 注意这种定义的形式是新特性中允许的
    set(target, key, new_value, receiver) {
        console.log(`原本的对象为: ${target}`)
        obj[key] = new_value
        console.log("属性值设置成功....")
    },
​
    get(target, key, receiver) {
        console.log(`原本的对象为: ${target}`)
        console.log(target[key])
    },
​
    deleteProperty(target, key) {
        console.log(`被删除的属性为: ${target[key]}`)
        delete obj[key]
    },
​
    has(target, key) {
        console.log(`进行判断是否存在的键为: ${key}, 目标对象是: ${target}`)
        return key in obj
    }
})
​
// 这里需要注意一点哈,就职我们一直没有提及的
// 在 ES6 之前,我们对象中的键只能是字符串(虽然书写的不是想象的使用 "" 包裹的字符串,但是任然是字符串的)
// 但是在 ES6 以及之后,就可以使字符串作为键或者说 Symbol 数据类型作为键了
console.log("name" in proxy_obj)
但是通过阅读源码,我们是可以知道的是: Proxy 和 Reflect 是实现的是联合工作的😄 😄 😄

为什么需要这么施工呐???

  • 我们需要明确一点的是,我们的代理对象的设置是为了我们不在对原对象进行任何的操作了
  • 但是上面的实现都是对我们的原对象 target 进行的操作,所以说上面的代码书写,从原则上就已经违背了 Proxy 的设计模型了

那么我们不这样操作,接下来该如何实现呐???

  • 这个时候就需要使用到我们的 Reflect 来实现操作代理对象了
  • 需要注意的一点是,实际上在我们的日常的开发中,我们使用的 Proxy 和 Reflect 的次数少之又少
  • 这个只是在框架的底层实现的源码中使用的比较多而已,这个就是我们的 Vue3 的响应式原理的底层实现使用的技术
let obj = {
    name: "76433",
    age: 18
}
​
const proxy_obj = new Proxy(obj, {
    /**
     * 使用 Reflect 和 Proxy 混合双打 的好处
     * 优势一: 代理对象的目的,不在操作原对象
     * 优势二: Reflect 的所有方法的返回值都是我们的 boolean,有利于我们后续的判断实行
     * 优势三: Reflect.set/get 方法的最后一个参数 receiver,可以实现决定对象访问其的 setter/getter 的this 指向
     */
    // 注意这种定义的形式是新特性中允许的
    set(target, key, new_value, receiver) {
        console.log(`原本的对象为: ${target}`)
        Reflect.set(target, key, new_value, receiver) ?
            console.log("属性值设置成功..."):
            console.log("属性值设置失败...")
    },
​
    get(target, key, receiver) {
        console.log(`原本的对象为: ${target}`)
        Reflect.get(target, key, receiver) ?
            console.log("对象中的值获取成功..."):
            console.log("对象中的值获取失败...")
    },
​
    deleteProperty(target, key) {
        console.log(`需要被删除的属性为: ${key}`)
        Reflect.deleteProperty(target, key) ?
            console.log("需要被删除的属性删除成功..."):
            console.log("需要被删除的对象删除失败...")
    },
​
    has(target, key) {
        console.log(`进行判断是否存在的键为: ${key}, 目标对象是: ${target}`)
        return Reflect.hasOwnProperty.call(key) ? "具有该属性": "不具有该属性"
    }
})
​
​
Reflect.deleteProperty(proxy_obj, "age")

自行研读 Reflect 的底层源码

declare namespace Reflect {
​
    function apply<T, A extends readonly any[], R>(
        target: (this: T, ...args: A) => R,
        thisArgument: T,
        argumentsList: Readonly<A>,
    ): R;
    function apply(target: Function, thisArgument: any, argumentsList: ArrayLike<any>): any;
​
    function construct<A extends readonly any[], R>(
        target: new (...args: A) => R,
        argumentsList: Readonly<A>,
        newTarget?: new (...args: any) => any,
    ): R;
    function construct(target: Function, argumentsList: ArrayLike<any>, newTarget?: Function): any;
​
    function defineProperty(target: object, propertyKey: PropertyKey, attributes: PropertyDescriptor & ThisType<any>): boolean;
​
    function deleteProperty(target: object, propertyKey: PropertyKey): boolean;
​
    function get<T extends object, P extends PropertyKey>(
        target: T,
        propertyKey: P,
        receiver?: unknown,
    ): P extends keyof T ? T[P] : any;
​
    function getOwnPropertyDescriptor<T extends object, P extends PropertyKey>(
        target: T,
        propertyKey: P,
    ): TypedPropertyDescriptor<P extends keyof T ? T[P] : any> | undefined;
​
    function getPrototypeOf(target: object): object | null;
​
    function has(target: object, propertyKey: PropertyKey): boolean;
​
    function isExtensible(target: object): boolean;
​
    function ownKeys(target: object): (string | symbol)[];
​
    function preventExtensions(target: object): boolean;
​
    function set<T extends object, P extends PropertyKey>(
        target: T,
        propertyKey: P,
        value: P extends keyof T ? T[P] : any,
        receiver?: any,
    ): boolean;
    function set(target: object, propertyKey: PropertyKey, value: any, receiver?: any): boolean;
​
    function setPrototypeOf(target: object, proto: object | null): boolean;
}
  • 已经把源码的注释剔除,这个时候,我们是可以发现一点的是我们的 Reflect 中的每一个方法的话,都是实现了返回了一个

    • boolean 类型的值的,这样设计的好处就是有助于我们的后续的根据 Reflect 方法的判断进行后续的进一步的操作
  • 同时在这里我们使用了一种思想,就是我们后面的 Typescript

    • 这里使用到了我们的泛型编程的思想,如果使用过 C++ 的templete 模板的话,这个很好理解的
    • 同时的话,这种书写格式如果使用的是我们的编译器打开,实际上的话和我们以前书写文档注释呈现的效果是大差不差的
    • 所以说这就是书写文档注释的好处吧,间接性的为我们的函数参数添加了类型,同时剔除的源码注释也是使用的文档注释

image-20241121044112155.png

总结

该部分,我们探究了关于 Proxy 创建代理对象和 Reflect 新的对象的使用

同时在讲解的时候带入了自己对 Vue3 响应式实现的基本的原理

同时需要注意的是,我们可能在平时的开发中利用这些技术的次数不多的

但是的话到后面的帮助自己研读 Vue3 等框架的源码是有很大的好处的

同时最后呐,浅浅的提及了关于我们的后期开发项目中会使用的 Typescript