• defineProperty 与 Proxy 的区别


    Object.defineProperty 用法

    此方法可以直接在对象上定义一个新属性,也可以修改对象的现有属性。
    语法:Object.defineProperty(obj, prop, descriptor)

    • obj:要定义或修改属性的对象
    • prop:要定义或修改的属性的名称或 Symbol
    • descriptor:该属性的描述符
    • 返回值:传入的对象obj

    该方法的核心在于属性描述符(descriptor 参数),存在两种写法,分别为数据描述符与存取描述符
    这两种描述符都是对象。它们共享以下键值:

    • configurable 表示该属性的描述符能否被改变以及该属性能否从对象上删除,默认为 false

    • enumerable 表示该属性是否会出现在对象的枚举属性中,默认为 false

    数据描述符还具有以下键值:

    • value 表示该属性的值,默认为 undefined

    • writable 表示该属性是否可写,默认为 false

    存取描述符还具有以下键值:

    • get 表示该属性的取函数,在读取改属性时会执行此函数,函数返回值作为属性的值

    • set 表示该属性的存函数,修改属性值时会调用此函数,并接受一个参数(被赋予的新值)

    两种描述符不可同时配置,如果一个描述符同时拥有 value 或 writable 和 get 或 set 键,则会产生一个异常
    用一个简单的示例展示其用法:

    let obj = {}
    
    Object.defineProperty(obj, '_num', {
      value: 1,
      writable: true,
      enumerable: false, // 不被枚举
      configurable: false,
    })
    Object.defineProperty(obj, 'num', {
      get() {
        console.log('num属性被访问')
        return this._num
      },
      set(value) {
        console.log('num属性被修改')
        this._num = value
      },
      enumerable: true,
      configurable: true,
    })
    
    console.log(obj._num) // 1
    console.log(obj.num) // num属性被访问 1
    console.log(Object.keys(obj)) // ['num'] (_num属性未被枚举)
    obj.num = 10 // num属性被修改
    delete obj._num // (configurable为false,无法删除)
    delete obj.num
    console.log(obj.num) // undefined
    console.log(obj._num) // 1
    
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31

    Proxy 用法

    Proxy 是一个类,可以为对象创建一个代理对象,从而实现对基本操作的拦截和自定义。
    语法:const p = new Proxy(target, handler)

    • target:要代理的对象
    • handler:拦截器对象
    • 返回值:新的代理对象

    该方法的核心在于拦截器对象,其包含许多的函数属性,用于定义代理行为。
    Proxy 可以拦截对象的 14 种操作,不过我们一般拦截的只有读写操作,语法如下:

    const hander = {
      get(target, property, receiver) {},
      set(target, property, value, receiver) {},
    }
    
    • 1
    • 2
    • 3
    • 4
    • target:被代理的原对象
    • property:要访问的属性名或 Symbol
    • value:要设置的新属性值
    • receiver:代理对象
    • 返回值:get 返回要获取的属性值,而 set 返回一个布尔值,表示赋值是否成功

    用一个简单的示例展示其用法:

    const obj = {
      _num: 1,
    }
    
    const hander = {
      get(target, property) {
        if (property === 'num') return target['_num']
        else return undefined
      },
      set(target, property, value) {
        if (property === 'num') {
          console.log('num属性赋值成功')
          target['_num'] = value
          return true
        } else {
          console.log(`${property}属性赋值失败`)
          return false
        }
      },
    }
    
    const p = new Proxy(obj, hander)
    
    console.log(p._num) // undefined
    console.log(p.num) // 1
    p.num = 2 // num属性赋值成功
    p._num = 3 // _num属性赋值失败
    console.log(p.num) // 2
    console.log(obj._num) // 2
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30

    两者区别

    看完了 Obejct.defineProperty 与 Proxy 的用法,接下来结合 Vue 讲解一下它们的区别

    属性值的存储位置

    在 Vue2 中,是通过闭包来存储属性值的

    // 属性值存储在 val 变量中
    const def = (obj, prop, val = obj[prop]) => {
      Object.defineProperty(obj, prop, {
        get() {
          console.log(`${prop}属性被访问`)
          return val
        },
        set(value) {
          console.log(`${prop}属性被修改为${value}`)
          val = value
        },
        enumerable: true,
        configurable: true,
      })
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    而在 Vue3 中,属性值一致存储在原对象中

    const hander = {
      get(target, prop) {
        console.log(`${prop}属性被访问`)
        return target[prop]
      },
      set(target, prop, value) {
        console.log(`${prop}属性被修改为${value}`)
        target[prop] = value
        return true
      },
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    拦截多个属性

    当使用 Obejct.defineProperty 拦截对象的多个属性时,需要遍历对象的所有属性名,依次设置描述符

    而使用 Proxy 拦截对象的多个属性时,拦截器会直接作用于所有属性

    Obejct.defineProperty

    const def = (obj, prop) => {...}
    
    const observe = (obj) => {
      // 循环所有属性名,Vue2为了兼容 IE,源码中使用的是 for in
      for (const prop of Object.keys(obj)) {
        def(obj, prop)
      }
      return obj
    }
    
    const obj = observe({
      a: 1,
      b: 2,
      c: 3,
    })
    
    obj.a // a属性被访问
    obj.b // b属性被访问
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    Proxy

    const hander = {...}
    
    const reactive = (obj) => {
      return new Proxy(obj, hander)
    }
    
    const obj = reactive({
      a: 1,
      b: 2,
      c: 3,
    })
    
    obj.a // a属性被访问
    obj.b // b属性被访问
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    多层对象

    二者在拦截多层对象的操作,也就是深度监听时,都需要递归地操作对象,区别在于 Vue2 在定义时就要完成所有层属性的拦截,而 Vue3 则是到真正使用时,才生成对应的 Proxy 对象。

    Vue2

    const def = (obj, prop, val = obj[prop]) => {
      // 值是对象,递归监听
      if (typeof val === 'object') observe(val)
    
      Object.defineProperty(obj, prop, {
        get() {
          console.log(`${prop}属性被访问`)
          return val
        },
        set(value) {
          console.log(`${prop}属性被修改为${value}`)
          val = value
        },
        enumerable: true,
        configurable: true,
      })
    }
    
    const observe = (obj) => {
      for (const prop of Object.keys(obj)) {
        def(obj, prop)
      }
      return obj
    }
    
    let obj = observe({
      a: {
        b: 1,
      },
    })
    obj.a.b
    // a属性被访问
    // b属性被访问
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33

    Vue3

    const hander = {
      get(target, key) {
        let val = target[key]
        // 值是对象,则进行包装后返回
        return typeof val === 'object' ? reactive(val) : val
      },
      set(target, key, value) {
        target[key] = value
        return true
      },
    }
    
    // 对象与代理的映射表,weakMap是为了不影响垃圾回收
    const weakMap = new WeakMap()
    
    const reactive = (obj) => {
      // 已经包装过了,返回现成的代理
      if (weakMap.has(obj)) return weakMap.get(obj)
    
      const p = new Proxy(obj, hander)
      weakMap.set(obj, p)
      return p
    }
    
    const obj = reactive({
      a: {
        b: 1,
      },
    })
    obj.a.b
    // a属性被访问
    // b属性被访问
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32

    监听数组

    Obejct.defineProperty 只能通过设置原型,改写数组方法来实现响应式,较为复杂,且无法实现通过数组索引的访问

    // 设置响应式数组的原型
    const arrayProto = Object.create(Array.prototype)
    // 要被改写的7个数组方法
    const methods = ['push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse']
    for (const methodName of methods) {
      // 备份原来的方法
      const original = [][methodName]
      // 定义新的方法
      arrayProto[methodName] = function (...args) {
        // 恢复原来的功能
        const result = original.apply(this, args)
        // 数组可能插入新项,也需要变为observe
        let inserted = []
        switch (methodName) {
          case 'push':
          case 'unshift':
            inserted = args
            break
          case 'splice':
            // splice格式是splice(下标,数量,插入的新项)
            inserted = args.slice(2)
            break
        }
        // 将新插入的项也变为响应式
        for (const newItem of inserted) {
          observe(newItem)
        }
    
        console.log(`调用数组的${methodName}方法`)
    
        return result
      }
    }
    
    const def = (obj, prop, val = obj[prop]) => {……}
    
    const observe = (obj) => {
      // 如果是数组,改变原型
      if (Array.isArray(obj)) Object.setPrototypeOf(obj, arrayProto)
      else
        for (const prop of Object.keys(obj)) {
          def(obj, prop)
        }
      return obj
    }
    
    const obj = observe({
      a: [0, 1, 2],
    })
    
    obj.a.push({ b: 3 })
    // a属性被访问
    // 调用数组的push方法
    
    obj.a[3].b
    // a属性被访问
    // b属性被访问
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57

    而 Proxy 能直接拦截对数组的所有操作,包括数字索引的访问

    const hander = {……}
    
    // 对象与代理的映射表,weakMap是为了不影响垃圾回收
    const weakMap = new WeakMap()
    
    const reactive = (obj) => {……}
    
    const obj = reactive({
      a: [1, 2, 3],
    })
    
    obj.a[0] = 2
    // a属性被访问
    // 0属性被修改为2
    
    obj.a.push(4)
    // push属性被访问
    // length属性被访问
    // 3属性被修改为4
    // length属性被修改为4
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
  • 相关阅读:
    git使用说明
    ElasticSearch聚合查询
    Java getResource
    Spark系列之Spark的RDD详解
    Windows安装最新SonarQube版本
    C++ 数列游戏
    element级联选择器中el-cascader通过props自定义设置value、label、children
    LVS集群
    NR 物理层编码 S2 - 汉明码
    【教学类-19-02】20221127《ABCABC式-规律排序-A4竖版2份》(中班)
  • 原文地址:https://blog.csdn.net/weixin_44368963/article/details/133784684