持续创作,加速成长!这是我参与「掘金日新计划 · 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优势
- 是ES5语法,对浏览器兼容性处理友好
Object.defineProperty缺点
- 对对象直接新增的属性,无法监听。
- 对数组未监听的索引以及部分数组的方法,无法监听。
- 对对象的深度监听,需要深度递归遍历整个对象,耗费性能。
vue2的Object.defineProperty
vue2在使用bject.defineProperty完成数据响应式时,有几个特殊点
-
对操作数组的方法重写并监听数据变化,指通过数组的方法push,pop,shift等操作数组时响应式仍然存在,而通过索引下标直接修改数组响应式是无效的。
-
对象属性是深度监听的,但是对对象添加新属性时,不具备响应式。
-
上述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>
上面例子清晰的解释了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优势
- 完美的支持对数组各种方法和索引的拦截监听
- 读对象新增属性支持拦截监听
- vue3中基于proxy动态拦截的方式对对象深度监听,也就是一开始不会全部拦截对象每个属性,当我们访问/操作属性时才会动态的拦截具体属性。
Proxy缺点
ES6语法,存在部分浏览器兼容性问题。