关于前端:es6-深入理解-Proxy-和-Reflect-这对影子兄弟

59次阅读

共计 10612 个字符,预计需要花费 27 分钟才能阅读完成。

前言

vue3.0 应用了 Proxy 对象做响应式零碎, 让很多人都意识了 Proxy 对象,vue3.0 中的响应式数据甚至是组件实例, 其实都是 Proxy 对象。然而这篇文章不是说 Proxy 对象在 vue3.0 是怎么发挥作用的, 而是说说其影子兄弟 Reflect。既然是影子, 天然是相映相随的。Reflect 中文解释能够是映射, 也正好阐明其设计初衷。简略来说 Proxy 对象代理并拦挡指标对象特定行为, 而这些行为都有其默认行为, 而 Reflect 对象就是不便能够实现这些默认行为,Proxy 有多少拦挡类型,Reflect 就实现了多少对应类型的映射函数 (Proxy 对象的拦挡类型, Reflect 对象的映射 API 如果对这个两个对象还不是很理解, 能够先看看下面链接中对这两个对象的介绍)
可能你会想, 我都晓得默认行为了, 自已实现不就能够了, 为什么还须要 Reflect, 为了阐明这个问题, 下文将针对 set 这个罕用拦挡类型去阐明, 如果不必 Reflect 去实现你将遇到多少问题, 能力胜利实现其默认行为。

存取器属性搭上 receiver 参数

根底实现

上面先定义根底的代码

  const target = {
      _name: '指标对象',
      get name() {return this._name},
      set name(name) {this._name = name},
      age: 18
    }
    const defaultProxy = new Proxy(target, {})
    const customProxy = new Proxy(target, {set(target, key, value, receiver) {target[key] = value
      }
    })

存取器属性

先看设置的 target 对象, 也就是代理的指标对象, 先看 name 属性, 能够看到该属性后面有 get set 标识, 这类属性叫存取器属性, 在拜访值和设置值时才会惰性执行对应的 getter setter 函数。
每一个一般对象属性都有一个对应属性形容对象, 上面是其属性描述符对象

Object.getOwnPropertyDescriptor(target, 'name')
{enumerable: true, configurable: true, get: ƒ, set: ƒ}

age 属性是一个数据属性, 其属性描述符对象是

Object.getOwnPropertyDescriptor(target, 'age')
{value: 18, writable: true, enumerable: true, configurable: true}

如果要让一个属性变为只读能够把数据属性的 writable 设置为 false, 或者把存取器属性的 set 设置为 undefined, 具体怎么设置属性的描述符对象下文会有提及, 以上只是略微说一下根底的常识, 上面回归正题

触发 set 拦挡的行为

defaultProxy 是一个默认的代理对象, 没有设置任何拦挡行为, 接着设置了一个 customProxy 对象并设置了 set 拦挡行为, 这个办法具备 4 个参数, 比拟有纳闷的可能是 receiver 参数, 上面通过例子就能够晓得该参数的作用了。set 办法外部的代码就是要实现 set 拦挡的默认行为, 如果不必 Reflect 对象, 实现其默认行为要遇到多少问题, 就是上面要具体阐明的内容。

在开始前先依据文档列出了能触发 set 拦挡行为的操作

  • Property assignment: proxy[foo] = bar and proxy.foo = bar
  • Inherited property assignment: Object.create(proxy)[foo] = bar 代理对象位于原型链上, 在这种状况下拦挡函数的 receiver 指向子类实例对象
  • Reflect.set() 精确来说是 Reflect.set(proxy, key, value, receiver?) 留神第一个参数是代理对象不是指标对象,receiver 是可选值, 不存在时指向第一个参数

根底实现的问题

先从最容易的实现开始

target[key] = value

先看默认行为, 这里是控制台输入

defaultProxy.name = 'hello world'
"hello world"
target.name
"hello world"
defaultProxy.age = 3 
3
target.age
3

再看实现的默认行为

customProxy.name = 'hello world'
"hello world"
customProxy.name
"hello world"
customProxy.age = 3
3
customProxy.age
3

看似都一样, 是不是就曾经胜利了? 上面咱们把 set name 批改一下, 让其读出 this

 set name(name) {console.log(this)
     this._name = name
  },

设置 defaultProxy.name = 'hello world' this 的输入是 Proxy {_name: "hello world", age: 18}
设置 customProxy.name = 'hello world' this 的输入是 {_name: "指标对象", age: 18}
设置 defaultProxy.name = 'hello world' this 的输入是 Proxy {_name: "hello world", age: 18}
设置 Reflect.set(defaultProxy, 'name', 'hello world') this 的输入是 Proxy {_name: "hello world", age: 18}
设置 Reflect.set(defaultProxy, 'name', 'hello world', { a: 1}) this 的输入是 {a: 1}
设置 Reflect.set(customProxy, 'name', 'hello world', { a: 1}) this 的输入是 {_name: "hello world", age: 18}
能够看出应用 cunstomProxy 其 this 值总是指向指标对象, 因为咱们的实现就是简略的对象属性赋值,this 天然就指向指标对象。而默认行为中,this 是指向代理对象的, 而通过 Reflect.set(defaultProxy, 'name', 'hello world', { a: 1})能够看出 this 更严格来说是指向 receiver 参数, 在没有传入时就取第一个参数的值。

修复存取器属性

很显著这里实现的默认行为是有问题的, 针对这类问题能够修复为

    set(target, key, value, receiver) {const descriptor = Object.getOwnPropertyDescriptor(target, key)
        if(descriptor.hasOwnProperty('set')) {
          const setter = descriptor.set
          if(typeof setter === 'function') {setter.call(receiver, value)
          }
        } else {target[key] = value
        }
      }

而后按下面的测试方法, 测试了一番, 发现都满足默认行为了, 然而到这步只解决了存取器属性的问题, 还有数据属性的还没有解决, 上面看看数据属性你会遇到什么问题。

隐匿的 defineProperty 陷阱拦挡

减少 defineProperty 拦挡

在开始介绍这个问题前, 先减少默认代理对象和实现代理对象一个 defineProperty 拦挡, 因为是针对 set 拦挡实现默认行为, 为了不引入其余问题, 间接就应用 return Reflect.defineProperty(target, property, descriptor) 实现 defineProperty 拦挡的默认行为

    const defaultProxy = new Proxy(target, {defineProperty(target, property, descriptor) {console.log(`defaultProxy defineProperty 拦挡触发 `, target, property, descriptor)
        return Reflect.defineProperty(target, property, descriptor)
      }
    })
    const customProxy = new Proxy(target, {set(target, key, value, receiver) {const descriptor = Object.getOwnPropertyDescriptor(target, key)
        if(descriptor.hasOwnProperty('set')) {
          const setter = descriptor.set
          if(typeof setter === 'function') {setter.call(receiver, value)
          }
        } else {target[key] = value
        }
      },
      defineProperty(target, property, descriptor) {console.log(`cunstomProxy defineProperty 拦挡触发 `, target, property, descriptor)
        return Reflect.defineProperty(target, property, descriptor)
      }
    })

剖析拦挡逻辑

上面执行默认行为

defaultProxy.name = 'hello world'
Proxy {_name: "指标对象", age: 18}
defaultProxy defineProperty 拦挡触发 {_name: "指标对象", age: 18} _name {value: "hello world"}
"hello world"
defaultProxy.age = 3
defaultProxy defineProperty 拦挡触发 {_name: "hello world", age: 18} age {value: 3}
3

上面执行实现行为

customProxy.name = 'hello world'
Proxy {_name: "指标对象", age: 18}
"hello world"
customProxy.age = 3
3

能够看出默认的代理行为在设置属性时会触发 defineProperty 拦挡, 而查看文档你会发现, 触发 defineProperty 拦挡的行为有

  • Object.defineProperty()
  • Reflect.defineProperty()

这里没有提到对象属性赋值也会触发 defineProperty 拦挡, 那这个触发行为应该是 set 默认行为所触发, 如果要实现该默认行为也须要在 set 拦挡中去实现。
仔细分析以上的输入能够发现, 触发 defineProperty 拦挡的属性是 _nameage, 并没有 name, _name 是因为在设置 name 属性时在对应的 setter 函数执行时进行赋值的, 从而执行对应的拦挡函数, 也就是在设置数据属性时会触发 defineProperty 拦挡, 而设置存取性属性时并不会触发 defineProperty 拦挡。

修复数据属性

基于下面的剖析, 让 set 拦挡触发 defineProperty 拦挡

    set(target, key, value, receiver) {const descriptor = Object.getOwnPropertyDescriptor(target, key)
        if(descriptor.hasOwnProperty('set')) {
          const setter = descriptor.set
          if(typeof setter === 'function') {setter.call(receiver, value)
          }
        } else {
          Object.defineProperty(receiver, key, {value: value})
        }
      },

再次运行测试

customProxy.name = 'hello world'
Proxy {_name: "指标对象", age: 18}
cunstomProxy defineProperty 拦挡触发 {_name: "指标对象", age: 18} _name {value: "hello world"}
"hello world"
customProxy.age = 3
cunstomProxy defineProperty 拦挡触发 {_name: "hello world", age: 18} age {value: 3}
3

以上的测试都是在批改曾经存在的属性, 如果减少属性, Object.getOwnPropertyDescriptor(target, key) 会返回 undefined, 执行 hasOwnProperty 办法是会报错的。
当一个属性应用对象属性赋值的形式设置时, 会默认设置为数据属性, 而且是可枚举可配置和可写的。
而且还有一点 Object.defineProperty 这个办法设置属性时, 如果属性是新属性, 那些疏忽设置的形容属性都默认为 false 或者是 undefined, 如果是曾经存在的属性, 疏忽设置的形容属性取原来的值, 例如

let o = {}
Object.defineProperty(o, 'value', { value: '2'})
{value: "2"}
Object.getOwnPropertyDescriptor(o, 'value')
{value: "2", writable: false, enumerable: false, configurable: false}
o.name = 'hh'
"hh"
Object.getOwnPropertyDescriptor(o, 'name')
{value: "hh", writable: true, enumerable: true, configurable: true}configurable: trueenumerable: truevalue: "hh"writable: true__proto__: Object
Object.defineProperty(o, 'name', { value: '3', enumerable: false})
{value: "2", name: "3"}
Object.getOwnPropertyDescriptor(o, 'name')
{value: "3", writable: true, enumerable: false, configurable: true}

基于这些要求从新批改 set 实现

    set(target, key, value, receiver) {const descriptor = Object.getOwnPropertyDescriptor(target, key)
        if(descriptor && descriptor.hasOwnProperty('set')) {
          const setter = descriptor.set
          if(typeof setter === 'function') {setter.call(receiver, value)
          }
        } else {
          Object.defineProperty(receiver, key, descriptor ? {value: value, // 为放弃和默认行为统一, 不能批改 descriptor.value 后把 descriptor 返回} : {
            value: value,
            writable: true,
            enumerable: true,
            configurable: true
          })
        }
      },

defineProperty 拦挡的作用

基于下面的实现, 也实现了在代理对象作用原型对象时, 把数据属性设置在子类实例身上。
看上面的例子

    const children = Object.create(target)
    children.name = 'hello world'
    console.log(children.hasOwnProperty('name')) // false
    console.log(children.hasOwnProperty('_name')) // true
    children.age = 3
    console.log(children.hasOwnProperty('age')) // true
    console.log(children.age) // 3
    console.log(target.age) // 18

    const children2 = Object.create(defaultProxy)
    children.name = 'hello world'
    console.log(children.hasOwnProperty('name')) // false
    console.log(children.hasOwnProperty('_name')) // true
    children.age = 3
    console.log(children.hasOwnProperty('age')) // true
    console.log(children.age) // 3
    console.log(target.age) // 18

    const children3 = Object.create(customProxy)
    children.name = 'hello world'
    console.log(children.hasOwnProperty('name')) // false
    console.log(children.hasOwnProperty('_name')) // true
    children.age = 3
    console.log(children.hasOwnProperty('age')) // true
    console.log(children.age) // 3
    console.log(target.age) // 18

当代理对象在原型链上时, 子类实例设置属性时, 一样会触发代理的拦挡行为, 这个时候 receiver 参数的值示意的是子类实例, 也就是这里的 children, 如果 children 设置的属性是存取器属性默认会调用原型链对象中对应属性的 getter setter 函数, 而并不会设置在 children 身上。如果是新属性或者是数据属性, 那么会设置在本身身上, 并不会批改原型链上的属性。如果不采纳 defineProperty 进行设置, 间接通过 receiver[key] = value 来实现, 就会呈现有限递归。
数据属性和存取器属性都修复了, 还有什么问题吗

Invariants

如果你有看文档, 你会发现每一个拦挡操作中都有一个 Invariants, 中文翻译为不变式。我的了解就是对拦挡行为的限度, 如果你的拦挡行为超出了限度, 就会报 TypeError
先来看看 set 拦挡的限度

  • Cannot change the value of a property to be different from the value of the corresponding target object property if the corresponding target object property is a non-writable, non-configurable data property.

这条限度是针对数据属性的, 如果设置的对象属性是非可配置和非可读的, 那么拦挡函数不能返回 true

  • Cannot set the value of a property if the corresponding target object property is a non-configurable accessor property that has undefined as its [[Set]] attribute

这条限度是针对存取器属性的, 如果设置的对象属性是非可配置且 set 为 undefined,, 那么拦挡函数不能返回 true

  • In strict mode, a false return value from the set() handler will throw a TypeError exception.

前两条都表明该属性如果是只读属性, 就应该是不能批改的, 最初一条就是在严格模式下如果返回 false 会触发报错

    const o = {}
    Object.defineProperty(o, 'level', {value: 5 // 没有设置的形容属性均为 false})
    Object.defineProperty(o, 'grade', {set: undefined // 没有设置的形容属性均为 false, get 为 undefined})
    const proxy = new Proxy(o, {set() {return true // 强行认为能够批改, 就会触发拦挡行为限度谬误}
    })
    // proxy.level = 666 // Uncaught TypeError: 'set' on proxy: trap returned truish for property 'level' which exists in the proxy target as a non-configurable and non-writable data property with a different value
    // proxy.grade = 666 // Uncaught TypeError: 'set' on proxy: trap returned truish for property 'grade' which exists in the proxy target as a non-configurable and non-writable accessor property without a setter

    void function () {
      'use strict'
      const proxy2 = new Proxy(o, {set() {return false}
      })
      proxy2.a = 1
      console.log(o) // Uncaught TypeError: 'set' on proxy: trap returned falsish for property 'a'
    }()

如果要正确触发这类默认行为, 那么在实现中须要正确 return 设置是否胜利的信息, 这里可能有点难了解, 能够看看这个例子

    const o = {}
    Object.defineProperty(o, 'level', {value: 5 // 没有设置的形容属性均为 false})
    const proxy = new Proxy(o, {})
    proxy.level = 6

这个并不会报错, 因为默认行为晓得了这个是不能设置的, 因而默认拦挡函数就返回了 false, 因而你如果要实现默认行为, 也须要返回一个正确设置与否的信息。不能设置就应该返回 false, 能够设置就应该返回 true。

返回设置信息

基于这点咱们再来看看有返回时的实现

       set(target, key, value, receiver) {const descriptor = Object.getOwnPropertyDescriptor(target, key)
        if (typeof descriptor === 'object') {if(descriptor.configurable === 'false' && (descriptor.writable === 'false' || descriptor.set === 'undefined')) {return false}
        }
        if(descriptor && descriptor.hasOwnProperty('set')) {
          const setter = descriptor.set
          if(typeof setter === 'function') {setter.call(receiver, value)
            return true 
          } 
        } else {
            Object.defineProperty(receiver, key, descriptor ? {value: value, // 为放弃和默认行为统一, 不能批改 descriptor.value 后把 descriptor 返回} : {
              value: value,
              writable: true,
              enumerable: true,
              configurable: true
            })
            return true
        } 
      },

上面测试一下

Object.defineProperty(target, 'level', {value: 5})
    void function() {
      'use strict'
      defaultProxy.level = 6 //Uncaught TypeError: Cannot assign to read only property 'level' of object '[object Object]'
    }()
    void function() {
      'use strict'
      customProxy.level = 6 // Uncaught TypeError: 'set' on proxy: trap returned falsish for property 'level'
    }()

尽管都按冀望那样报错了, 然而报错的内容不同, 很显著默认行为还执行了其余的操作。

总结

最初, 其实还是没有实现 set 的实在默认行为, 然而通过这个例子, 也能够分明理解到, 要模仿实在的默认拦挡行为是很简单的, 而 Reflect 设计进去就是为了解决这样的难题。
上面就是正确的实现

      set(target, key, value, receiver) {return Reflect.set(target, key, value, receiver)
        }

如果应用了拦挡函数, 并须要执行默认行为, 如果要放弃代码的健壮性, 传入的参数都须要全副传入到 Reflect 对象内, 如果漏掉 receiver, receiver 会被当做第一个参数, 这样就不会触发 defineProperty 拦挡, 这个就违反了默认行为

Reflect.set(target, 'age', 3)
true
Reflect.set(target, 'age', 3, defaultProxy)
defaultProxy defineProperty 拦挡触发 {_name: "指标对象", age: 3} age {value: 3}
true

还须要留神的是须要 return 该返回值, 不然不会正确触发 Invariants 的限度行为

这里举的是 set 拦挡的例子, 其余的拦挡例子, 如果须要调用默认行为也都是同样的操作, 留神两点

  • Reflect 中传入全副参数
  • return Reflect 的执行后果

参考

  • https://developer.mozilla.org…
  • https://developer.mozilla.org…

正文完
 0