Proxy-Reflect使用详解

455 阅读5分钟

一、Proxy

Proxy 对象用于创建一个对象的代理,从而实现基本操作的拦截和自定义(如属性查找、赋值、枚举、函数调用等)

1.1 Object.defineProperty

  • Object.defineProperty 通过存储属性描述符来对对象属性的操作进行监听
    • Object.defineProperty设计的初衷,不是为了去监听截止一个对象中所有的属性的,是定义普通的属性,但是后面我们强行将它变成了数据属性描述符

    • 如果监听更加丰富的操作,比如新增属性、删除属性,Object.defineProperty是无能为力的

    const obj = {
      name: "lilei",
      age: 18,
      height: 1.78
    }
    
    // 需求: 监听对象属性的所有操作
    // 监听所有的属性: 遍历所有的属性, 对每一个属性使用defineProperty
    const keys = Object.keys(obj)
    for (const key of keys) {
      let value = obj[key]
      Object.defineProperty(obj, key, {
        set: function(newValue) {
          console.log(`监听: 给${key}设置了新的值:`, newValue)
          value = newValue
        },
        get: function() {
          console.log(`监听: 获取${key}的值`)
          return value
        }
      })
    }
    
    // console.log(obj.name)
    // obj.name = "hanmeimei"
    console.log(obj.age)
    obj.age = 17
    console.log(obj.age)
    
    // 新增和删除监听不到
    obj.adress = "北京市"
    delete obj.height
    

1.2 Proxy基本使用

  • proxy监听对象
    • 监听一个对象的相关操作,先创建一个代理对象(Proxy对象),之后对该对象的所有操作,都通过代理对象来完成,代理对象可以监听对原对象进行哪些操作
  • new Proxy(target, handler)

    • const p = new Proxy(target, handler)
  • 想要侦听某些具体的操作,那么就可以在handler中添加对应的捕捉器(Trap)

    • set函数有四个参数:

      • target:目标对象(侦听的对象)
      • property:将被设置的属性key
      • value:新属性值
      • receiver:调用的代理对象
    • get函数有三个参数:

      • target:目标对象(侦听的对象)
      • property:被获取的属性key
      • receiver:调用的代理对象
    const obj = {
      name: "lilei",
      age: 18,
      height: 1.78
    }
    
    
    // 1.创建一个Proxy对象
    const objProxy = new Proxy(obj, {
      set: function(target, key, newValue) {
        console.log(`监听: 监听${key}的设置值: `, newValue)
        target[key] = newValue
      },
      get: function(target, key) {
        console.log(`监听: 监听${key}的获取`)
        return target[key]
      }
    })
    
    // 2.对obj的所有操作, 应该去操作objProxy
    console.log(objProxy.name)
    objProxy.name = "hanmeimei"
    console.log(objProxy.name)
    objProxy.name = "jim"
    
    objProxy.address = "北京市"
    console.log(objProxy.address)
    

1.3 Proxy捕获器

  • handler.has():in 操作符的捕捉器

  • handler.get():属性读取操作的捕捉器

  • handler.set():属性设置操作的捕捉器

  • handler.deleteProperty():delete 操作符的捕捉器

  • handler.getPrototypeOf():Object.getPrototypeOf 方法的捕捉器

  • handler.setPrototypeOf():Object.setPrototypeOf 方法的捕捉器

  • handler.isExtensible():Object.isExtensible 方法的捕捉器(判断是否可以新增属性)

  • handler.preventExtensions():Object.preventExtensions 方法的捕捉器(阻止对象的扩展)

  • handler.getOwnPropertyDescriptor():Object.getOwnPropertyDescriptor 方法的捕捉器

  • handler.defineProperty():Object.defineProperty 方法的捕捉器

  • handler.ownKeys():Object.getOwnPropertyNames 方法和 Object.getOwnPropertySymbols 方法的捕捉器

    const obj = {
      name: "lilei",
      age: 18,
      height: 1.78
    }
    
    // 1.创建一个Proxy对象
    const objProxy = new Proxy(obj, {
      set: function(target, key, newValue) {
        console.log(`监听: 监听${key}的设置值: `, newValue)
        target[key] = newValue
      },
      get: function(target, key) {
        console.log(`监听: 监听${key}的获取`)
        return target[key]
      },
    
      deleteProperty: function(target, key) {
        console.log(`监听: 监听删除${key}属性`)
        delete obj.name
      },
    
      has: function(target, key) {
        console.log(`监听: 监听in判断 ${key}属性`)
        return key in target
      }
    })
    
    delete objProxy.name
    console.log("age" in objProxy)
    

1.4 apply/construct(应用于函数对象)

  • handler.apply():函数调用操作的捕捉器。
  • handler.construct():new 操作符的捕捉器
    function foo(num1, num2) {
      console.log(this, num1, num2)
    }
    
    const fooProxy = new Proxy(foo, {
      apply: function(target, thisArg, otherArgs) {
        console.log("监听执行了apply操作")
        target.apply(thisArg, otherArgs)
      },
      construct: function(target, otherArgs) {
        console.log("监听执行了new操作")
        console.log(target, otherArgs)
        return new target(...otherArgs)
      }
    })
    
    // fooProxy.apply("abc", [111, 222])
    new fooProxy("aaa", "bbb")
    

二、 Reflect

Reflect 是一个内置的对象,它提供拦截 JavaScript 操作的方法。这些方法与Proxy的方法相同。Reflect不是一个函数对象,因此它是不可构造的。

2.1 Reflect 和 Object

  • Reflect 提供了很多操作JavaScript对象的方法,有点像Object中操作对象的方法
    • Reflect.getPrototypeOf(target)类似于 Object.getPrototypeOf()
    • Reflect.defineProperty(target, propertyKey, attributes)类似于Object.defineProperty()
  • 为什么还需要有Reflect对象
    • 因为在早期的ECMA规范中没有考虑到这种对 对象本身 的操作如何设计会更加规范,所以将这些API放到了Object上面,但是Object作为一个构造函数,这些操作实际上放到它身上并不合适,另外还包含一些类似于 indelete操作符,让JS看起来是会有一些奇怪的;

    • 所以在ES6中新增了Reflect,这些操作都集中到了Reflect对象上,另外在使用Proxy时,可以做到不操作原对象。

  • Object和Reflect对象之间的API关系,可以参考MDN文档

2.2 Object方法存在问题

  • Object.defineProperty() 在设置对象的属性为不可配置时,仍设置对象的某个属性,并不知道是否定义成功
  • Object.defineProperty() 在设置对象的属性为不可配置时,删除对象的某个属性,在严格模式下会报错
    "use strict"
    
    const obj = {
      name: "kobe",
      age: 24
    }
    
    Object.defineProperty(obj, "name", {
      configurable: false
    })
    
    delete obj.name // Uncaught TypeError: Cannot delete property 'name' of #<Object>
    if (obj.name) {
      console.log("name没有删除成功")
    } else {
      console.log("name删除成功")
    }
    

2.3 Reflect -> 返回布尔值

  • Reflect.defineProperty() 在设置对象的属性时会返回一个布尔值

    "use strict"
    
    const obj = {
      name: "kobe",
      age: 24
    }
    
    Object.defineProperty(obj, "name", {
      configurable: false
    })
    if (Reflect.deleteProperty(obj, "name")) {
      console.log("name删除成功")
    } else {
      console.log("name没有删除成功")
    }
    
  • 常见方法:与 Proxy 对应

    • Reflect.has(target, propertyKey):判断一个对象是否存在某个属性,和 in 运算符 的功能完全相同

    • Reflect.get(target, propertyKey[, receiver]):获取对象身上某个属性的值,类似于 target[name]

    • Reflect.set(target, propertyKey, value[, receiver]):将值分配给属性的函数。返回一个Boolean,如果更新成功,则返回true

    • Reflect.deleteProperty(target, propertyKey):作为函数的delete操作符,相当于执行 delete target[name]

2.4 Reflect和Proxy一起使用

  • 代理对象的目的:
    1. 不直接操作原对象
    2. 有返回值
    3. 改变访问器中的this指向
    const obj = {
      name: "kobe",
      age: 24
    }
    
    const objProxy = new Proxy(obj, {
      set: function(target, key, newValue) {
        // target[key] = newValue
        // 1.好处一: 代理对象的目的: 不再直接操作原对象
        // 2.好处二: Reflect.set方法有返回Boolean值, 可以判断本次操作是否成功
        const isSuccess = Reflect.set(target, key, newValue)
    
        if (!isSuccess) {
          throw new Error(`set ${key} failure`)
        }
      },
      get: function(target, key) {
    
      }
    })
    // 操作代理对象
    objProxy.name = "james"
    console.log(obj)
    
  • Proxy中的receiver
    • 如果源对象(obj)有setter、getter的访问器属性,那么可以通过receiver来改变里面的this
    const obj = {
      _name: "kobe",
      set name(newValue) {
        console.log("this:", this) // 默认是obj
        this._name = newValue
      },
      get name() {
        return this._name
      }
    }
    
    // obj.name = "aaaa"
    
    // console.log(obj.name)
    // obj.name = "james"
    
    const objProxy = new Proxy(obj, {
      set: function(target, key, newValue, receiver) {
        // 1.好处一: 代理对象的目的: 不再直接操作原对象
        // 2.好处二: Reflect.set方法有返回Boolean值, 可以判断本次操作是否成功
        /*
           3.好处三:
             > receiver就是外层Proxy对象
             > Reflect.set/get最后一个参数, 可以决定对象访问器setter/getter的this指向
        */
        console.log("proxy中设置方法被调用")
        const isSuccess = Reflect.set(target, key, newValue, receiver)
    
        if (!isSuccess) {
          throw new Error(`set ${key} failure`)
        }
      },
      get: function(target, key, receiver) {
        console.log("proxy中获取方法被调用")
        return Reflect.get(target, key, receiver)
      }
    })
    
    // 操作代理对象
    objProxy.name = "james"
    console.log(objProxy.name)
    

2.4 Reflect的construct用法

  • 有点像 new 操作符构造函数 , 相当于运行 new target(...args)
    function Person(name, age) {
      this.name = name
      this.age = age
    }
    
    function Student(name, age) {
      // Person.call(this, name, age)
      const _this = Reflect.construct(Person, [name, age], Student)
      return _this
    }
    
    // const stu = Reflect.construct(Person, ["kobe", 24], Student)
    const stu = new Student("kobe", 24)
    console.log(stu) // Student {name: 'kobe', age: 24}
    console.log(stu.__proto__ === Student.prototype) // true