Object.defineProperty()函数:在一个对象上定义一个新的属性,或者修改一个对象已有的属性,并返回这个对象。1.基本使用语法:Object.defineProperty(obj,prop,descriptor)参数:要定义或修改的属性名称或[Symbol]对要定义或修改的属性的描述见一个简单的例子letperson={}letpersonName='lihua'//给person对象加上属性namep,值为personNameObject.defineProperty(person,'namep',{//但是默认是不可枚举的(因为in打印不能printed),canbe:enumerable:true//默认不可修改,canbe:wirtable:true//默认不可删除,canbe:configurable:trueget:function(){console.log('triggeredgetmethod')returnpersonName},set:function(val){console.log('触发了set方法')personName=val}})//读取person对象的namp属性时,触发get方法console.log(person.namep)//修改personName时,重新访问person.namep,发现修改成功personName='liming'console.log(person.namep)//修改person.namep和trigger设置方法person.namep='huahua'console。log(person.namep)\通过这个方法,我们成功监听到了person上name属性的变化。2、监控对象上的多个属性在上面的用法中,我们只监控了一个属性的变化,但在实际情况中,我们通常需要同时监控多个属性的变化。这时候我们就需要配合Object.keys(obj)进行遍历。此方法可以返回obj对象上所有可枚举属性的字符数组。(实用forin遍历也是可以的)下面是API的简单使用效果:varobj={0:'a',1:'b',2:'c'};console.log(Object.keys(obj));//console:['0','1','2']使用这个API,我们可以遍历被劫持对象的所有属性,但是如果只是一个简单的结合上面的思路和API,我们会发现并不能达到效果,下面是我写的错误版本:Object.keys(person).forEach(function(key){Object.defineProperty(person,key,{enumerable:true,configurable:true,//defaultThisget(){returnperson[key]},set(val){console.log(`person中的${key}属性已被修改`)person[key]=val//修改后可以进行渲染操作}})})console.log(person.age)上面的代码好像没什么问题,但是试试运行吧~你会跟我一样溢出栈.为什么是这样?让我们关注get方法。当我们访问person的属性时,会触发get方法并返回person[key]。但是访问person[key]也会触发get方法,导致递归调用,最终栈溢出。这也导致了我们下面的方法,我们需要设置一个中转Observer,让get中的返回值不直接访问obj[key]。letperson={name:'',age:0}//实现一个响应函数functiondefineProperty(obj,key,val){Object.defineProperty(obj,key,{get(){console.log(`visited${key}attribute`)returnval},set(newVal){console.log(`${key}attributehasbeenchangedto${newVal}`)val=newVal}})}//实现一个遍历函数ObserverfunctionObserver(obj){Object.keys(obj).forEach((key)=>{defineProperty(obj,key,obj[key])})}Observer(person)console.log(person.age)person.age=18console.log(person.age)3.对象的深度监控那么我们如何解决对象嵌套在对象中的情况呢?其实在上面代码的基础上,加一个递归就可以轻松实现了~我们可以观察到,其实Observer就是我们要实现的监控功能。我们预期的目标是:只要传入对象,就可以对这个对象实现属性监听,即使对象的属性也是一个对象。我们在defineProperty()函数中加入递归的情况:functiondefineProperty(obj,key,val){//如果一个对象的属性也是一个对象,则递归进入该对象并监听if(typeofval==='object'){observer(val)}Object.defineProperty(obj,key,{get(){console.log(`访问${key}属性`)returnval},set(newVal){console.log(`${key}属性已经改为${newVal}`)val=newVal}})}当然我们还需要在observer中加入递归停止条件:functionObserver(obj){//如果输入不是对象,returnif(typeofobj!=="object"||obj===null){return}//for(keyinobj){Object.keys(obj).forEach((key)=>{defineProperty(obj,key,obj[key])})//}}其实到这里就解决的差不多了,就是还有个小问题。如果修改了一个属性,如果原来的属性值是一个字符串,但是我们重新分配了一个对象,我们如何监控新添加的对象的所有属性呢?其实很简单,修改set函数即可:set(newVal){//如果newVal是对象,则递归进入对象监听if(typeofval==='object'){observer(key)}console.log(`${key}属性已经改成${newVal}`)val=newVal}到此大功告成~4.监听数组那么如果对象的属性是数组呢?我们如何实施监控?请看下面这段代码:letarr=[1,2,3]letobj={}//使用arr作为obj的属性来监听Object.defineProperty(obj,'arr',{get(){console.log('getarr')returnarr},set(newVal){console.log('set',newVal)arr=newVal}})console.log(obj.arr)//输出getarr[1,2,3]正常obj.arr=[1,2,3,4]//输出set[1,2,3,4]正常obj.arr.push(3)//输出getarr异常,我们无法监听推。我们发现通过push方法向数组中添加元素,set方法是监听不到的。其实通过索引访问或者修改数组中已经存在的元素可以启动get和set,但是对于通过push和unshiftelement添加的元素,会添加一个索引。在这种情况下,需要手动初始化,以便监控新添加的元素。另外,通过pop或shift删除一个元素,会删除和更新索引,也会触发setter和getter方法。在Vue2.x中,这个问题是通过重写Array原型上的方法来解决的,这里就不说了。有兴趣的话,uu可以多了解一下~Proxy是不是感觉有点复杂?其实在上面的描述中,我们还有一个问题没有解决:就是当我们要给对象增加一个新的属性时,我们也需要手动去监听这个新的属性。也正是这个原因,在使用vue给数组或者data中的对象添加属性的时候,需要使用vm.$set来保证新增的属性也是响应式的。可见通过Object.definePorperty()进行数据监控比较麻烦,需要大量的人工处理。这也是为什么尤雨熙在Vue3.0中改用Proxy的原因。接下来我们就来看看Proxy是如何解决这些问题的~1.基本使用语法:constp=newProxy(target,handler)参数:target:要和Proxy一起打包的target对象(可以是任何类型的对象,包括native数组、函数,甚至是另一个代理)处理程序:通常有一个函数作为属性的对象,每个属性中的函数定义了代理在执行各种操作时的行为p。通过Proxy,我们可以拦截对设置了代理的对象的一些操作。外界对这个对象的各种操作都要先经过这一层拦截。(类似defineProperty)先看一个简单的例子//定义一个需要代理的对象letperson={age:0,school:'Xidian'}//定义handler对象lethander={get(obj,key){//如果对象中有这个属性,返回属性值,如果没有,返回默认值66returnkeyinobj?obj[key]:66},set(obj,key,val){obj[key]=valreturntrue}}//把handler对象传入ProxyletproxyObj=newProxy(person,hander)//测试get是否能成功拦截console.log(proxyObj.age)//输出0console.log(proxyObj.school)//输出西电控制台.log(proxyObj.name)//输出默认值66//测试设置是否能成功拦截proxyObj.age=18console.log(proxyObj.age)//输出18修改成功可见Proxy代理是整个对象,而不是具体对象的属性,不需要我们通过遍历一个一个进行数据绑定。值得注意的是:我们在使用Object.defineProperty()给对象添加属性后,我们对该对象属性的读写操作还是在对象本身。但是一旦使用了Proxy,如果我们想让读写操作生效,就需要对Proxy实例对象proxyObj进行操作。另外,在MDN上明确指出set()方法应该返回一个boolean值,否则会报TypeError。2、轻松解决Object.defineProperty中遇到的问题上面在使用Object.defineProperty时,我们遇到的问题是:1、一次只能监控一个属性,需要遍历所有的属性来监控。我们已经在上面解决了这个问题。2、当对象的属性还是对象时,需要递归监控。3.对于对象的新属性,需要手动监控。4、对于数组的push和unshift方法添加的元素,无法监听这些问题。这些问题在Proxy中很容易解决。让我们看一下下面的代码。查第二题根据上面的代码,我们把对象的结构做的稍微复杂一点。letperson={age:0,school:'Xidian',children:{name:'小明'}}lethander={get(obj,key){returnkeyinobj?obj[key]:66},set(obj,key,val){obj[key]=valreturntrue}}letproxyObj=newProxy(person,hander)//测试getconsole.log(proxyObj.children.name)//输出:小明console.log(proxyObj.children.height)//输出:undefined//测试setproxyObj.children.name='菜菜'console.log(proxyObj.children.name)//输出:菜菜可以看到children对象上的name属性监听成功(至于为什么children.height是undefined,可以再讨论)查看第三个问题这个其实在基本使用中有提到,访问的proxyObj.name是原对象上不存在的属性,但是我们访问的时候还是可以拦截到得到。考第四题letsubject=['高数']lethandler={get(obj,key){returnkeyinobj?obj[key]:'没有这个题目'},set(obj,key,val){obj[key]=val//set方法成功要返回true,否则会报错returntrue}}letproxyObj=newProxy(subject,handler)//检查get和setconsole.log(proxyObj)//输出['highnumber']console.log(proxyObj[1])//输出没有这个主题proxyObj[0]='大学物理'console.log(proxyObj)//输出['大学物理']////检查是否可以监听push添加的元素proxyObj.push('LinearAlgebra')console.log(proxyObj)//Output['大学物理','线性代数']至此,我们之前的问题就完美解决了。3、Proxy支持13种拦截操作。Proxy除了get和set拦截read和assignment操作外,还支持拦截其他各种action。下面简单介绍一下。如果你想了解更多,可以去MDN看看。get(target,propKey,receiver):拦截对象属性的读取,比如proxy.foo和proxy['foo']。set(target,propKey,value,receiver):拦截对象属性的设置,比如proxy.foo=v或者proxy['foo']=v,返回一个布尔值。has(target,propKey):拦截propKey在proxy中的操作,返回一个布尔值。deleteProperty(target,propKey):拦截deleteproxy[propKey]操作,返回一个布尔值。ownKeys(target):拦截Object.getOwnPropertyNames(proxy)、Object.getOwnPropertySymbols(proxy)、Object.keys(proxy)、for...in循环,返回数组。该方法返回目标对象的所有属性的属性名,而Object.keys()的返回结果只包含目标对象本身的可遍历属性。getOwnPropertyDescriptor(target,propKey):拦截Object.getOwnPropertyDescriptor(proxy,propKey),返回属性的描述对象。defineProperty(target,propKey,propDescs):拦截Object.defineProperty(proxy,propKey,propDescs),Object.defineProperties(proxy,propDescs),返回一个布尔值。preventExtensions(target):拦截Object.preventExtensions(proxy),返回一个布尔值。getPrototypeOf(target):拦截Object.getPrototypeOf(proxy),返回一个对象。isExtensible(target):拦截Object.isExtensible(proxy),返回一个布尔值。setPrototypeOf(target,proto):拦截Object.setPrototypeOf(proxy,proto),返回一个布尔值。如果目标对象是一个函数,那么还有两个额外的动作可以被拦截。apply(target,object,args):拦截Proxy实例作为函数调用的操作,如proxy(...args),proxy.call(object,...args),proxy.apply(...).construct(target,args):拦截调用一个Proxy实例作为构造函数的操作,比如newproxy(...args)。4.Proxy中关于this的问题虽然Proxy已经完成了对目标对象的代理,但是它并不是透明代理,也就是说:即使handler是一个空对象(即不做任何代理),他代理的对象this点不是对象,而是proxyObj对象。我们来看一个例子:lettarget={m(){//检查this是否指向proxyObkjconsole.log(this===proxyObj)}}lethandler={}letproxyObj=newProxy(target,handler)proxyObj.m()//Output:truetarget.m()//Output:false可以看到代理对象target里面的this指向proxyObj。这种指向有时会引起问题。我们看下面的例子:const_name=newWeakMap();classPerson{//将人的名字存储到_nameconstructor(name){_name.set(this,name);}的name属性中;}//当获取一个人的name属性,返回_namegetname(){return_name.get(this);}}constjane=newPerson('Jane');jane.name//'Jane'constproxyObj=newProxy(jane,{});proxyObj.name//undefined上面的例子中jane对象的name属性的获取依赖于this的指向,而this指向的是proxyObj,所以不能进行正常的代理。另外,一些js内置对象的内部属性只能通过纠正this来获取,所以Proxy无法代理这些原生对象的属性。请看下面的例子:consttarget=newDate();consthandler={};constproxyObj=newProxy(target,handler);proxyObj.getDate();//TypeError:thisisnotaDateobject。可以看到,通过proxy访问Date对象proxygetDate方法会报错,因为getDate方法只能在Date对象实例上获取,如果这不是Date对象实例,就会报错.那么我们如何解决这个问题呢?只需手动将其绑定到Date对象实例,请参见以下示例:consttarget=newDate('2015-01-01');consthandler={get(target,prop){if(prop==='getDate'){returntarget.getDate.bind(target);}returnReflect.get(target,prop);}};constproxy=newProxy(target,handler);proxy.getDate()//1结束,我的总结结束~文章是不是很全面,还有很多地方没有提到,比如:Proxy经常和Reflect一起使用我们常用的Object.create()方法将Proxy实例对象添加到Object对象的原型中,这样我们可以直接使用Object.proxyObj,有兴趣的可以尝试在Proxy的get和set中添加输出。你会发现,当我们调用push方法时,get和set会分别输出两次,这是为什么呢?学无止境,让我们一起努力吧~参考文章:1.Proxy和Object.defineProperty的介绍与比较2.MDNProxy
