女朋友让我给她讲讲Object.definePerporty与Proxy的区别

982 阅读8分钟

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

一:引言

随着vue框架被国内外用户热情追捧,Object.definePerporty与Proxy也随之成为时代的宠儿,二者的话题也是日进斗升,关于二者的讨论与描述也是铺天盖地。为了让大家从本质上理解它们,今天晚上我就辛苦下,好好给各位女朋友们讲讲Object.definePerporty与Proxy的区别与联系。

二:Object.definePerporty

2.1MDN介绍

静态方法object. defineproperty()直接在对象上定义新属性,或者修改对象上的现有属性,并返回对象。

官方的解释已经十分十分明确和具体了,Object.definePerporty作用就是操作对象的属性,其中包含增删改查。

2.2 配置对象的简单使用

Object.defineProperty(obj,key,config)有三个参数,第一个参数是目标对象,第二个参数是目标对象的key,第三个参数是一个对象,其内部包含对对象属性key的增删改查配置。

api

Object.defineProperty(obj,key,{
  value:xxx,
  writable:false,//是否可修改,默认是false
  enumerable:false,//是否可枚举,默认是false
  configurable:false//是否可删除,默认是false
})

示例

let obj = {
  name:"dzp"
}

Object.defineProperty(obj,"age",{
  value:22,
  writable:false,//是否可修改,默认是false
  enumerable:false,//是否可枚举,默认是false
  configurable:false//是否可删除,默认是false
})

/*测试修改*/
obj.name = "dzp2"//可以修改
obj.age = 23//无法修改
console.log(obj.name,obj.age)//dzp2,22

/*测试枚举*/
for(let key in obj){
    console.log(key,obj[key]) //name dzp 
}

/*测试删除*/
delete obj.name //可以删除
delete obj.age //删除无效

console.log(obj.name,obj.age) //undefined,22

上面的例子的结果清晰的证明了通过Object.defineProperty配置的对象属性默认情况下是无法删除,无法修改,无法枚举(指for in和Object.keys遍历)。因此我们可以看到age属性只能读,而无法增删改,当然我们可以修改第三个参数修改age操作的权限。同时我们发现对象obj本身自己的属性name是不受任何影响的,依然可以增删改查。

总结

通过Object.defineProperty操作对象的属性可以配置其值,是否可遍历,是否可修改,是否可删除。但是永远不会影响对象其它未通过Object.defineProperty配置属性的增删改查。

2.3 配置对象的第二种方式

上面介绍的是Object.defineProperty操作对象属性的第一种方式,但最常用操作对象的方式是如下的方式二get和set。值得注意的是get方法相当于方式一中的value,set方法相当于方式一中的writable,因此get,set与value,writable不能同时出现,只能选择其中一种配置方式。

api

let obj = {}
let value = 22
Object.defineProperty(obj,"age",{
  //get等价于value
  get(){
    return value
  },
  
  //set等价于设置writable:true
  set(val){
    value = val   
  },
  enumerable:false,//是否可枚举,默认是false
  configurable:false//是否可删除,默认是false
})

consoe.log(obj.age)//22
obj.age = 23
console.log(obj.age)//23,可以修改

delete obj.age //删除无效

for(let key in obj) {} //无法枚举到age属性

2.4 对数组和对象的监听

Object.defineProperty常常被用来做对象的监听,下面我们模拟下如下操作

1.对象的监听

首先我们介绍下错误的对象监听方式,也是小伙伴们开始容易犯错的地方。相比大家都疑惑为什么不使用操作对象的这种方式监听?拿get举例子,这种直接通过对象获取的方式又会触发get方法,进而陷入无限循环中。同理set也是如此。

let obj = {
    name:"dzp"
}
Object.defineProperty(obj,"name",{
  get(){
    return obj.name
  },
  set(val) {
    obj.name = val
  }
})
console.log(obj.name)
obj.name="xxx"

如下是正确的对象监听方式,直接通过for in 遍历取出对象的所有属性和值,然后逐个监听。观察下面的结果不难发现defineProperty对对象的监听只能监听到对象的第一层上,如果对象是多层的,则从第二层对象开始就无法被监听到,其实解决办法也比较简单,通过递归完成。

let obj = {
  name:"dzp",
  age:22,
  a:{b:1}
}

for(let key in obj ){
  let val = obj[key]
  Object.defineProperty(obj,key,{
    get(){
      console.log("get监听")
      return val
    },
    set(newVal){
      console.log("set监听")
      val = newVal
    }
  })
}
console.log(obj.name)//get监听 dzp
console.log(obj.age)//get监听 22
console.log(obj.a.b)//get监听 1
obj.name = "xxx"//set监听
obj.age = 1000//set监听
obj.a.b = 999//无法监听

2.对象深度监听

上面例子1介绍了对象的浅度监听方式,但实际开发中我们需要对对象的任意属性都可以监听,例如vue2的数据响应式就是深度监听。

function observeObj(obj) {
  for(let key in obj) {
    let val = obj[key]
    if(typeof val!=='object') {
      Object.defineProperty(obj,key,{
        get(){
          console.log("get拦截")
          return val
        },
        set(newVal){
          console.log("set拦截")
          val = newVal
        }
      })
    }else {
      observeObj(val)
    }
  }
}

let obj = {
  name:"dzp",
  age:22,
  a:{b:1}
}
observeObj(obj)
console.log(obj.name,obj.age,obj.a.b)
obj.a.b = 2//set成功拦截

很明显,经过深度监听的对象在其对象改变时可以成功的被监听到。

3.数组的监听

数组也属于对象,我们可以把数组的索引作为对象的key,因此我们也可以实现对数组的监听。同时我们注意到数组自带的部分方法在操作数组时无法被监听。

let arr = [1,2,3]
arr.forEach((item,index)=>{
  Object.defineProperty(arr,index,{
    get(){
      console.log("get监听")
      return item
    },
    set(val) {
      console.log("set监听")
      item = val
    }
  })
})
console.log(arr[0])//get监听 1
arr[0] = 100//set监听
arr[3] = 300//没有的索引无法监听
arr.push(4)//无法监听

2.5 vue2中Object.defineProperty的讨论

本节我们继续讨论vue2中使用Object.defineProperty实现数据响应式的例子。我们先总结下Object.defineProperty的优缺点

Object.defineProperty优势

  1. 是ES5语法,对浏览器兼容性处理友好

Object.defineProperty缺点

  1. 对对象直接新增的属性,无法监听。
  2. 对数组未监听的索引以及部分数组的方法,无法监听。
  3. 对对象的深度监听,需要深度递归遍历整个对象,耗费性能。

vue2的Object.defineProperty

vue2在使用bject.defineProperty完成数据响应式时,有几个特殊点

  1. 对操作数组的方法重写并监听数据变化,指通过数组的方法push,pop,shift等操作数组时响应式仍然存在,而通过索引下标直接修改数组响应式是无效的。

  2. 对象属性是深度监听的,但是对对象添加新属性时,不具备响应式。

  3. 上述1和2在响应式丢失时,可以通过this.$set(data,key,newValue)让数组/对象添加的新数据重新具备响应式。

     <template>
       <div id="app">
         <div>----------------对象的测试------------------</div>
         <div>name:{{name}}</div>
         <div>a:{{info.a}}</div>
         <div>b:{{info?.b}}</div>
         <div @click="changeName()">点击修改name</div>
         <div @click="changeAb()">点击修改info.a</div>
         <div @click="addAttr()">点击添加新属性b</div>
    
    
         <div>----------------数组的测试------------------</div>
         <div v-for="(item,index) in arr" :key="index">{{item}}</div>
         <div @click="()=>arr.push(4)">数组方法测试</div>
         <div @click="changeArr()">索引操作测试</div>
       </div>
     </template>
    
     <script>
    
     import Vue from "vue";
     export default {
       name: 'App',
       data() {
         return {
           info:{
             a:1
           },
           name:"dzp",
           arr:[1,2,3]
         }
       },
       methods:{
         changeName(){
           this.name = "xxx"
         },
         changeAb(){
           this.info.a = 100
         },
         addAttr(){
           //this.info.b = 999响应式无效
           this.$set(this.info,"b",999)
         },
         changeArr(){
           //this.arr[0] = 999 响应式无效
           this.$set(this.arr,0,999)
         }
       }
     }
     </script>
     
     
    

3.gif

上面例子清晰的解释了vue2响应式的特点,数组通过下标操作 和对象添加新属性时响应式会丢失,此时我们通过this.$set会让数组/对象属性重新具备响应式。其内部的原理就是通过Object.defineProperty对新属性添加了监听。

三:Proxy

终于来到了今天最后一个角色,它就是靓仔,吊毛-->Proxy。它究竟有咋样的魅力和惊人之处,会让vue3作者基于它重写响应式,我们今天一探究竟吧。

3.1 api使用

let obj = {a:1}

let p = new Proxy(obj,{
    get(target,key){
        return target[key]
    },
    set(target,key,val){
        target[key] = val
    }
})

可以发现Porxy的使用和Object.definePerporty还是比较类似的,特殊的是Porxy代理的对象会返回一个新对象,我们需要基于这个新对象操作属性才可以拦截对象的属性。

3.2 监听对象测试

let obj = {
  name:"dzp",
  age:22,
  a:{b:1}
}
let p = new Proxy(obj,{
  get(o,key){
    console.log("get")
    return o[key]
  },
  set(o,key,val){
    console.log("set")
    o[key] = val
  },
})

console.log(p.name) //get dzp
console.log(p.age) //get 22
console.log(p.a.b) //get 1

p.name = "xxx" //set
p.age = 99 //set
p.info = "info"//set
p.a.b = 100 //未触发set


可以看到Proxy对对象的监听时,优势十分明显,给对象添加新属性时,proxy是可以拦截到。但是同时发现了一个问题,Porxy对对象默认是监听第一层,当第二层对象修改数据时是监听不到的,此时仍然需要借助递归完成对象深度监听。

3.3 监听数组测试

let arr = [1,2,3]
let p = new Proxy(arr,{
  get(target,key){
    console.log("get")
    return target[key]
  },
  set(target,key,val){
    console.log("set")
    target[key] = val
  }
})
console.log(p[0])//get
p[0] = 9//set
p.push(4)//get get set

可以看到Porxy对数组的监听是十分优秀的,数组通过索引查询和修改,通过数组push等方法修改数组时都可以完美的监听到数组的改变。至于数组push为什么会触发多次get和set,这个和push底层源码有关系,大佬可自行钻研。

3.4 vue3中proxy思考

经过上面的分析,大家可能疑惑,proxy对比Object.definePerporty好像也就是对数组的索引和方法支持了拦截,对于对象也仅仅是优化了当对象新增属性时可以监听到,但是只能监听到对象的第一层,那vue3为什么敢称proxy性能大大优于Object.definePerporty?

回答

vue3使用proxy对对象进行监听时,确实只能监听第一层,当使用到/修改对象深层的某个属性时,它才会继续对当前层继续添加proxy拦截,如下所示,初始只能监听到name属性的修改,当我们访问obj.a.b或修改其值时,它才会动态的给obj的a对象继续添加一层proxy拦截,相对于Object.definePerporty岂不是提高了许多性能。

let obj = {name:"dzp",a:{b:1}}

proxy优势

  1. 完美的支持对数组各种方法和索引的拦截监听
  2. 读对象新增属性支持拦截监听
  3. vue3中基于proxy动态拦截的方式对对象深度监听,也就是一开始不会全部拦截对象每个属性,当我们访问/操作属性时才会动态的拦截具体属性。

Proxy缺点

ES6语法,存在部分浏览器兼容性问题。