详谈Object.defineProperty 及实现数据双向绑定
Object.defineProperty()和Proxy对象,都可以用来对数据的劫持操作。何为数据劫持呢?就是在我们访问或者修改某个对象的某个属性的时候,通过一段代码进行拦截行为,然后进行额外的操作,然后返回结果。那么vue中双向数据绑定就是一个典型的应用。
Vue2.x是使用Object.defindProperty(),来进行对对象的监听的。
Vue3.x版本之后就改用Proxy进行实现的。
下面我们先来理解下Object.defineProperty作用。
一:理解Object.defineProperty的语法和基本作用。
在理解之前,我们先来看看一个普通的对象,对象它是由多个名/值对组成的无序集合。对象中每个属性对于任意类型的值。
比如现在我们想创建一个简单的对象,可以简单的如下代码:
constobj=newObject;//或constobj={}; obj.name='kongzhi'; console.log(obj.name);//在控制台中会打印kongzhi obj.xxx=function(){ console.log(111); } //调用xxx方法 obj.xxx();//在控制台中会打印111
但是除了上面添加对象属性之外,我们还可以使用Object.defineProperty来定义新的属性或修改原有的属性。最终会返回该对象。
接下来我们慢慢来理解下该用法。
基本语法:
Object.defineProperty(obj,prop,descriptor);
基本的参数解析如下:
obj:可以理解为目标对象。
prop:目标对象的属性名。
descriptor:对属性的描述。
那么对于第一个参数obj和prop参数,我们很容易理解,比如上面的实列demo,我们定义的obj对象就是第一个参数的含义,我们在obj中定义的name属性和xxx属性是prop的含义,那么第三个参数描述符是什么含义呢?
descriptor:属性描述符,它是由两部分组成,分别是:数据描述符和访问器描述符,数据描述符的含义是:它是一个包含属性的值,并说明这个属性值是可读或不可读的对象。访问器描述符的含义是:包含该属性的一对getter/setter方法的对象。
下面我们继续来理解下数据描述符和访问器描述符具体包含哪些配置项含义及用法。
1.1数据描述符
constobj={ name:'kongzhi' }; //对obj对象已有的name属性添加数据描述 Object.defineProperty(obj,'name',{ configurable:true|false, enumerable:true|false, value:'任意类型的值', writable:true|false }); //对obj对象添加新属性的描述 Object.defineProperty(obj,'newAttr',{ configurable:true|false, enumerable:true|false, value:'任意类型的值', writable:true|false });
如上代码配置,数据描述符有如上configurable,enumerable,value及writable配置项。
下面我们来看下每个描述符中每个属性的含义:
1)value
属性对应的值,值的类型可以是任意类型的。比如我先定义一个obj对象,里面有一个属性name值为'kongzhi',现在我们通过如下代码改变obj.name的值,如下代码:
constobj={ name:'kongzhi' }; //对obj对象已有的name属性添加数据描述 Object.defineProperty(obj,'name',{ value:'1122' }); console.log(obj.name);//输出1122
如果上面我不设置value描述符值的话,那么它返回的值还是kongzhi的。比如如下代码:
constobj={ name:'kongzhi' }; //对obj对象已有的name属性添加数据描述 Object.defineProperty(obj,'name',{ }); console.log(obj.name);//输出kongzhi
2)writable
writable的英文的含义是:'可写的',在该配置中它的含义是:属性的值是否可以被重写,设置为true可以被重写,设置为false,是不能被重写的,默认为false。
如下代码:
constobj={}; Object.defineProperty(obj,'name',{ 'value':'kongzhi' }); console.log(obj.name);//输出kongzhi //改写obj.name的值 obj.name=111; console.log(obj.name);//还是打印出kongzhi
上面代码中使用Object.defineProperty定义obj.name的值value='kongzhi',然后我们使用obj.name进行重新改写值,再打印出obj.name可以看到值还是为kongzhi,这是Object.defineProperty中writable默认为false,不能被重写,但是下面我们将它设置为true,就可以进行重写值了,如下代码:
constobj={}; Object.defineProperty(obj,'name',{ 'value':'kongzhi', 'writable':true }); console.log(obj.name);//输出kongzhi //改写obj.name的值 obj.name=111; console.log(obj.name);//设置writable为true的时候打印出改写后的值111
3)enumerable
此属性的含义是:是否可以被枚举,比如使用for..in或Object.keys()这样的。设置为true可以被枚举,设置为false,不能被枚举,默认为false.
如下代码:
constobj={ 'name1':'xxx' }; Object.defineProperty(obj,'name',{ 'value':'kongzhi', 'writable':true }); //枚举obj的属性 for(constiinobj){ console.log(i);//打印出name1 }
如上代码,对象obj本身有一个属性name1,然后我们使用Object.defineProperty给obj对象新增name属性,但是通过forin循环出来后可以看到只打印出name1属性了,那是因为enumerable默认为false,它里面的值默认是不可被枚举的。但是如果我们将它设置为true的话,那么Object.defineProperty新增的属性也是可以被枚举的,如下代码:
constobj={ 'name1':'xxx' }; Object.defineProperty(obj,'name',{ 'value':'kongzhi', 'writable':true, 'enumerable':true }); //枚举obj的属性 for(constiinobj){ console.log(i);//打印出name1和name }
4)configurable
该属性英文的含义是:可配置的意思,那么该属性的含义是:是否可以删除目标属性。如果我们设置它为true的话,是可以被删除。如果设置为false的话,是不能被删除的。它默认值为false。
比如如下代码:
constobj={ 'name1':'xxx' }; Object.defineProperty(obj,'name',{ 'value':'kongzhi', 'writable':true, 'enumerable':true }); //使用delete删除属性 deleteobj.name; console.log(obj.name);//打印出kongzhi
如上代码使用delete命令删除obj.name的话,该属性值是删除不了的,因为configurable默认为false,不能被删除的。但是如果我们把它设置为true,那么就可以进行删除了。
如下代码:
constobj={ 'name1':'xxx' }; Object.defineProperty(obj,'name',{ 'value':'kongzhi', 'writable':true, 'enumerable':true, 'configurable':true }); //使用delete删除属性 deleteobj.name; console.log(obj.name);//打印出undefined
如上就是数据描述符中的四个配置项的基本含义。那么下面我们来看看访问器描述符的具体用法和含义。
1.2访问器描述符
访问器描述符的含义是:包含该属性的一对getter/setter方法的对象。如下基本语法:
constobj={}; Object.defineProperty(obj,'name',{ get:function(){}, set:function(value){}, configurable:true|false, enumerable:true|false });
注意:使用访问器描述符中getter或setter方法的话,不允许使用writable和value这两个配置项。
getter/setter
当我们需要设置或获取对象的某个属性的值的时候,我们可以使用setter/getter方法。
如下代码的使用demo.
constobj={}; letinitValue='kongzhi'; Object.defineProperty(obj,'name',{ //当我们使用obj.name获取该值的时候,会自动调用get函数 get:function(){ returninitValue; }, set:function(value){ initValue=value; } }); //我们来获取值,会自动调用Object.defineProperty中的get函数方法。 console.log(obj.name);//打印出kongzhi //设置值的话,会自动调用Object.defineProperty中的set方法。 obj.name='xxxxx'; console.log(obj.name);//打印出xxx
注意:configurable和enumerable配置项和数据描述符中的含义是一样的。
1.3:使用Object.defineProperty来实现一个简单双向绑定的demo
如下代码:
标题 {{name}}
1.4Object.defineProperty对数组的监听
看如下demo代码来理解下对数组的监听的情况。
constobj={}; letinitValue=1; Object.defineProperty(obj,'name',{ set:function(value){ console.log('set方法被执行了'); initValue=value; }, get:function(){ returninitValue; } }); console.log(obj.name);//1 obj.name=[];//会执行set方法,会打印信息 //给obj中的name属性设置为数组[1,2,3],会执行set方法,会打印信息 obj.name=[1,2,3]; //然后对obj.name中的某一项进行改变值,不会执行set方法,不会打印信息 obj.name[0]=11; //然后我们打印下obj.name的值 console.log(obj.name); //然后我们使用数组中push方法对obj.name数组添加属性不会执行set方法,不会打印信息 obj.name.push(4); obj.name.length=5;//也不会执行set方法
如上执行结果我们可以看到,当我们使用Object.defineProperty对数组赋值有一个新对象的时候,会执行set方法,但是当我们改变数组中的某一项值的时候,或者使用数组中的push等其他的方法,或者改变数组的长度,都不会执行set方法。
也就是如果我们对数组中的内部属性值更改的话,都不会触发set方法。
因此如果我们想实现数据双向绑定的话,我们就不能简单地使用obj.name[1]=newValue;这样的来进行赋值了。
那么对于vue这样的框架,那么一般会重写Array.property.push方法,并且生成一个新的数组赋值给数据,这样数据双向绑定就触发了。
因此我们需要重新编写数组的push方法来实现数组的双向绑定,我们可以参照如下方法来理解下。
1)重写编写数组的方法:
constarrPush={}; //如下是数组的常用方法 constarrayMethods=[ 'push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse' ]; //对数组的方法进行重写 arrayMethods.forEach((method)=>{ constoriginal=Array.prototype[method]; arrPush[method]=function(){ console.log(this); returnoriginal.apply(this,arguments); } }); consttestPush=[]; //对testPush的原型指向arrPush,因此testPush也有重写后的方法 testPush.__proto__=arrPush; testPush.push(1);//打印[],this指向了testPush testPush.push(2);//打印[1],this指向了testPush
2)使用Object.defineProperty对数组方法进行监听操作。
因此我们需要把上面的代码继续修改下进行使用Object.defineProperty进行监听即可:
Vue中的做法如下,代码如下:
functionObserver(data){ this.data=data; this.walk(data); } varp=Observer.prototype; vararrayProto=Array.prototype; vararrayMethods=Object.create(arrayProto); [ 'push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse' ].forEach(function(method){ //使用Object.defineProperty进行监听 Object.defineProperty(arrayMethods,method,{ value:functiontestValue(){ console.log('数组被访问到了'); constoriginal=arrayProto[method]; //使类数组变成一个真正的数组 constargs=Array.from(arguments); original.apply(this,args); } }); }); p.walk=function(obj){ letvalue; for(letkeyinobj){ //使用hasOwnProperty判断对象本身是否有该属性 if(obj.hasOwnProperty(key)){ value=obj[key]; //递归调用,循环所有的对象 if(typeofvalue==='object'){ //并且该值是一个数组的话 if(Array.isArray(value)){ constaugment=value.__proto__?protoAugment:copyAugment; augment(value,arrayMethods,key); observeArray(value); } /* 如果是对象的话,递归调用该对象,递归完成后,会有属性名和值,然后对 该属性名和值使用Object.defindProperty进行监听即可 */ newObserver(value); } this.convert(key,value); } } } p.convert=function(key,value){ Object.defineProperty(this.data,key,{ enumerable:true, configurable:true, get:function(){ console.log(key+'被访问到了'); returnvalue; }, set:function(newVal){ console.log(key+'被重新设置值了'+'='+newVal); //如果新值和旧值相同的话,直接返回 if(newVal===value)return; value=newVal; } }); } functionobserveArray(items){ for(leti=0,l=items.length;i以上这篇详谈Object.defineProperty及实现数据双向绑定就是小编分享给大家的全部内容了,希望能给大家一个参考,也希望大家多多支持毛票票。