作者
晴栀
“陪伴是最长情的告白”
?在这里插入图片描述??「作者:晴栀Sunset」
?必须记住我们学习的时间是有限的。时间有限,不只由于人生短促,更由于人的纷繁。我们应该力求把我们所有的时间用去做最有益的事
??基础知识
Object.defineProperty()
语法
描述
示例
创建属性
修改属性
添加多个属性和默认值
自定义Setters和Getters
继承属性
Proxy
术语
语法
参数
方法
示例
视图
使用Object.defineProperty
步骤
使用Proxy代理捕获
总结
手写MVVM双向数据绑定基础知识Object.defineProperty()?Object.defineProperty()方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回此对象。
??备注:应当直接在Object构造器对象上调用此方法,而不是在任意一个Object类型的实例上调用。
?constobject1={};Object.defineProperty(object1,property1,{value:42,writable:false});object1.property1=77;//throwsanerrorinstrictmodeconsole.log(object1.property1);//expectedoutput:42在这里插入图片描述语法?
Object.defineProperty(obj,prop,descriptor)
obj要定义属性的对象。prop要定义或修改的属性的名称或Symbol。descriptor要定义或修改的属性描述符。返回值被传递给函数的对象。?描述?该方法允许精确地添加或修改对象的属性。通过赋值操作添加的普通属性是可枚举的,在枚举对象属性时会被枚举到(for...in或Object.keys方法),可以改变这些属性的值,也可以删除这些属性。这个方法允许修改默认的额外选项(或配置)。默认情况下,使用Object.defineProperty()添加的属性值是不可修改(immutable)的。
??对象里目前存在的属性描述符有两种主要形式:数据描述符和存取描述符。数据描述符是一个具有值的属性,该值可以是可写的,也可以是不可写的。存取描述符是由getter函数和setter函数所描述的属性。一个描述符只能是这两者其中之一;不能同时是两者。
??这两种描述符都是对象。它们共享以下可选键值(默认值是指在使用Object.defineProperty()定义属性时的默认值):
??configurable当且仅当该属性的configurable键值为true时,该属性的描述符才能够被改变,同时该属性也能从对应的对象上被删除。「默认为」「false」。enumerable当且仅当该属性的enumerable键值为true时,该属性才会出现在对象的枚举属性中。「默认为false」。数据描述符还具有以下可选键值:value该属性对应的值。可以是任何有效的JavaScript值(数值,对象,函数等)。「默认为undefined」。writable当且仅当该属性的writable键值为true时,属性的值,也就是上面的value,才能被赋值运算符改变。「默认为false。」存取描述符还具有以下可选键值:get属性的getter函数,如果没有getter,则为undefined。当访问该属性时,会调用此函数。执行时不传入任何参数,但是会传入this对象(由于继承关系,这里的this并不一定是定义该属性的对象)。该函数的返回值会被用作属性的值。「默认为undefined」。set属性的setter函数,如果没有setter,则为undefined。当属性值被修改时,会调用此函数。该方法接受一个参数(也就是被赋予的新值),会传入赋值时的this对象。「默认为undefined」。?描述符默认值汇总?拥有布尔值的键configurable、enumerable和writable的默认值都是false。属性值和函数的键value、get和set字段的默认值为undefined。?描述符可拥有的键值?configurable``enumerable``value``writable``get``set数据描述符可以可以可以可以不可以不可以存取描述符可以可以不可以不可以可以可以?如果一个描述符不具有value、writable、get和set中的任意一个键,那么它将被认为是一个数据描述符。如果一个描述符同时拥有value或writable和get或set键,则会产生一个异常。
记住,这些选项不一定是自身属性,也要考虑继承来的属性。为了确认保留这些默认值,在设置之前,可能要冻结Object.prototype,明确指定所有的选项,或者通过Object.create(null)将__proto__属性指向null。
//使用__proto__varobj={};vardescriptor=Object.create(null);//没有继承的属性//默认没有enumerable,没有configurable,没有writabledescriptor.value=static;Object.defineProperty(obj,key,descriptor);//显式Object.defineProperty(obj,"key",{enumerable:false,configurable:false,writable:false,value:"static"});//循环使用同一对象functionwithValue(value){vard=withValue.d
(withValue.d={enumerable:false,writable:false,configurable:false,value:null});d.value=value;returnd;}//...并且...Object.defineProperty(obj,"key",withValue("static"));//如果freeze可用,防止后续代码添加或删除对象原型的属性//(value,get,set,enumerable,writable,configurable)(Object.freeze
Object)(Object.prototype);示例?
如果你想了解如何使用Object.defineProperty方法和类二进制标记语法,可以看看这些额外示例。
?创建属性?如果对象中不存在指定的属性,Object.defineProperty()会创建这个属性。当描述符中省略某些字段时,这些字段将使用它们的默认值。
?varo={};//创建一个新对象//在对象中添加一个属性与数据描述符的示例Object.defineProperty(o,"a",{value:37,writable:true,enumerable:true,configurable:true});//对象o拥有了属性a,值为37//在对象中添加一个设置了存取描述符属性的示例varbValue=38;Object.defineProperty(o,"b",{//使用了方法名称缩写(ES特性)//下面两个缩写等价于://get:function(){returnbValue;},//set:function(newValue){bValue=newValue;},get(){returnbValue;},set(newValue){bValue=newValue;},enumerable:true,configurable:true});o.b;//38//对象o拥有了属性b,值为38//现在,除非重新定义o.b,o.b的值总是与bValue相同//数据描述符和存取描述符不能混合使用Object.defineProperty(o,"conflict",{value:0x9f,get(){return0xdeadbeef;}});//抛出错误TypeError:valueappearsonlyindatadescriptors,getappearsonlyinaccessordescriptors修改属性?
如果属性已经存在,Object.defineProperty()将尝试根据描述符中的值以及对象当前的配置来修改这个属性。如果旧描述符将其configurable属性设置为false,则该属性被认为是“不可配置的”,并且没有属性可以被改变(除了单向改变writable为false)。当属性不可配置时,不能在数据和访问器属性类型之间切换。
??当试图改变不可配置属性(除了value和writable属性之外)的值时,会抛出TypeError,除非当前值和新值相同。
?Writable属性?当writable属性设置为false时,该属性被称为“不可写的”。它不能被重新赋值。
?varo={};//创建一个新对象Object.defineProperty(o,a,{value:37,writable:false});console.log(o.a);//logs37o.a=25;//Noerrorthrown//(itwouldthrowinstrictmode,//evenifthevaluehadbeenthesame)console.log(o.a);//logs37.Theassignmentdidntwork.//strictmode(function(){usestrict;varo={};Object.defineProperty(o,b,{value:2,writable:false});o.b=3;//throwsTypeError:"b"isread-onlyreturno.b;//returns2withoutthelineabove}());
如示例所示,试图写入非可写属性不会改变它,也不会引发错误。
Enumerable属性?enumerable定义了对象的属性是否可以在for...in循环和Object.keys()中被枚举。
?varo={};Object.defineProperty(o,"a",{value:1,enumerable:true});Object.defineProperty(o,"b",{value:2,enumerable:false});Object.defineProperty(o,"c",{value:3});//enumerable默认为falseo.d=4;//如果使用直接赋值的方式创建对象的属性,则enumerable为trueObject.defineProperty(o,Symbol.for(e),{value:5,enumerable:true});Object.defineProperty(o,Symbol.for(f),{value:6,enumerable:false});for(variino){console.log(i);}//logsaandd(inundefinedorder)Object.keys(o);//[a,d]o.propertyIsEnumerable(a);//trueo.propertyIsEnumerable(b);//falseo.propertyIsEnumerable(c);//falseo.propertyIsEnumerable(d);//trueo.propertyIsEnumerable(Symbol.for(e));//trueo.propertyIsEnumerable(Symbol.for(f));//falsevarp={...o}p.a//1p.b//undefinedp.c//undefinedp.d//4p[Symbol.for(e)]//5p[Symbol.for(f)]//undefinedConfigurable属性
configurable特性表示对象的属性是否可以被删除,以及除value和writable特性外的其他特性是否可以被修改。
varo={};Object.defineProperty(o,a,{get(){return1;},configurable:false});Object.defineProperty(o,a,{configurable:true});//throwsaTypeErrorObject.defineProperty(o,a,{enumerable:true});//throwsaTypeErrorObject.defineProperty(o,a,{set(){}});//throwsaTypeError(setwasundefinedpreviously)Object.defineProperty(o,a,{get(){return1;}});//throwsaTypeError//(eventhoughthenewgetdoesexactlythesamething)Object.defineProperty(o,a,{value:12});//throwsaTypeError//(valuecanbechangedwhenconfigurableisfalsebutnotinthiscaseduetogetaccessor)console.log(o.a);//logs1deleteo.a;//Nothinghappensconsole.log(o.a);//logs1
如果o.a的configurable属性为true,则不会抛出任何错误,并且,最后,该属性会被删除。
添加多个属性和默认值考虑特性被赋予的默认特性值非常重要,通常,使用点运算符和Object.defineProperty()为对象的属性赋值时,数据描述符中的属性默认值是不同的,如下例所示。
varo={};o.a=1;//等同于:Object.defineProperty(o,"a",{value:1,writable:true,configurable:true,enumerable:true});//另一方面,Object.defineProperty(o,"a",{value:1});//等同于:Object.defineProperty(o,"a",{value:1,writable:false,configurable:false,enumerable:false});自定义Setters和Getters
下面的例子展示了如何实现一个自存档对象。当设置temperature属性时,archive数组会收到日志条目。
functionArchiver(){vartemperature=null;vararchive=[];Object.defineProperty(this,temperature,{get:function(){console.log(get!);returntemperature;},set:function(value){temperature=value;archive.push({val:temperature});}});this.getArchive=function(){returnarchive;};}vararc=newArchiver();arc.temperature;//get!arc.temperature=11;arc.temperature=13;arc.getArchive();//[{val:11},{val:13}]
下面这个例子中,getter总是会返回一个相同的值。
varpattern={get:function(){returnIalwayreturnthisstring,whateveryouhaveassigned;},set:function(){this.myname=thisismynamestring;}};functionTestDefineSetAndGet(){Object.defineProperty(this,myproperty,pattern);}varinstance=newTestDefineSetAndGet();instance.myproperty=test;//Ialwayreturnthisstring,whateveryouhaveassignedconsole.log(instance.myproperty);//thisismynamestringconsole.log(instance.myname);继承属性?
如果访问者的属性是被继承的,它的get和set方法会在子对象的属性被访问或者修改时被调用。如果这些方法用一个变量存值,该值会被所有对象共享。
?functionmyclass(){}varvalue;Object.defineProperty(myclass.prototype,"x",{get(){returnvalue;},set(x){value=x;}});vara=newmyclass();varb=newmyclass();a.x=1;console.log(b.x);//1
这可以通过将值存储在另一个属性中解决。在get和set方法中,this指向某个被访问和修改属性的对象。
functionmyclass(){}Object.defineProperty(myclass.prototype,"x",{get(){returnthis.stored_x;},set(x){this.stored_x=x;}});vara=newmyclass();varb=newmyclass();a.x=1;console.log(b.x);//undefined?
不像访问者属性,值属性始终在对象自身上设置,而不是一个原型。然而,如果一个不可写的属性被继承,它仍然可以防止修改对象的属性。
?functionmyclass(){}myclass.prototype.x=1;Object.defineProperty(myclass.prototype,"y",{writable:false,value:1});vara=newmyclass();a.x=2;console.log(a.x);//2console.log(myclass.prototype.x);//1a.y=2;//Ignored,throwsinstrictmodeconsole.log(a.y);//1console.log(myclass.prototype.y);//1Proxy
「Proxy」对象用于创建一个对象的代理,从而实现基本操作的拦截和自定义(如属性查找、赋值、枚举、函数调用等)。
术语handler
包含捕捉器(trap)的占位符对象,可译为处理器对象。
traps
提供属性访问的方法。这类似于操作系统中捕获器的概念。
target
被Proxy代理虚拟化的对象。它常被作为代理的存储后端。根据目标验证关于对象不可扩展性或不可配置属性的不变量(保持不变的语义)。
语法constp=newProxy(target,handler)参数
target
要使用Proxy包装的目标对象(可以是任何类型的对象,包括原生数组,函数,甚至另一个代理)。
handler
一个通常以函数作为属性的对象,各属性中的函数分别定义了在执行各种操作时代理p的行为。
方法Proxy.revocable()
创建一个可撤销的Proxy对象。
handler对象的方法handler对象是一个容纳一批特定属性的占位符对象。它包含有Proxy的各个捕获器(trap)。
所有的捕捉器是可选的。如果没有定义某个捕捉器,那么就会保留源对象的默认行为。
handler.getPrototypeOf()
Object.getPrototypeOf方法的捕捉器。
handler.setPrototypeOf()
Object.setPrototypeOf方法的捕捉器。
handler.isExtensible()
Object.isExtensible方法的捕捉器。
handler.preventExtensions()
Object.preventExtensions方法的捕捉器。
handler.getOwnPropertyDescriptor()
Object.getOwnPropertyDescriptor方法的捕捉器。
handler.defineProperty()
Object.defineProperty方法的捕捉器。
handler.has()
in操作符的捕捉器。
handler.get()
属性读取操作的捕捉器。
handler.set()
属性设置操作的捕捉器。
handler.deleteProperty()
delete操作符的捕捉器。
handler.ownKeys()
Object.getOwnPropertyNames方法和Object.getOwnPropertySymbols方法的捕捉器。
handler.apply()
函数调用操作的捕捉器。
handler.construct()
new操作符的捕捉器。
一些不标准的捕捉器已经被废弃并且移除了。
示例基础示例在以下简单的例子中,当对象中不存在属性名时,默认返回值为37。下面的代码以此展示了gethandler的使用场景。
consthandler={get:function(obj,prop){returnpropinobj?obj[prop]:37;}};constp=newProxy({},handler);p.a=1;p.b=undefined;console.log(p.a,p.b);//1,undefinedconsole.log(cinp,p.c);//false,37无操作转发代理
在以下例子中,我们使用了一个原生JavaScript对象,代理会将所有应用到它的操作转发到这个对象上。
lettarget={};letp=newProxy(target,{});p.a=37;//操作转发到目标console.log(target.a);//37.操作已经被正确地转发验证
通过代理,你可以轻松地验证向一个对象的传值。下面的代码借此展示了sethandler的作用。
letvalidator={set:function(obj,prop,value){if(prop===age){if(!Number.isInteger(value)){thrownewTypeError(Theageisnotaninteger);}if(value){thrownewRangeError(Theageseemsinvalid);}}//Thedefaultbehaviortostorethevalueobj[prop]=value;//表示成功returntrue;}};letperson=newProxy({},validator);person.age=;console.log(person.age);//person.age=young;//抛出异常:UncaughtTypeError:Theageisnotanintegerperson.age=;//抛出异常:UncaughtRangeError:Theageseemsinvalid扩展构造函数
方法代理可以轻松地通过一个新构造函数来扩展一个已有的构造函数。这个例子使用了construct和apply。
functionextend(sup,base){vardescriptor=Object.getOwnPropertyDescriptor(base.prototype,"constructor");base.prototype=Object.create(sup.prototype);varhandler={construct:function(target,args){varobj=Object.create(base.prototype);this.apply(target,obj,args);returnobj;},apply:function(target,that,args){sup.apply(that,args);base.apply(that,args);}};varproxy=newProxy(base,handler);descriptor.value=proxy;Object.defineProperty(base.prototype,"constructor",descriptor);returnproxy;}varPerson=function(name){this.name=name};varBoy=extend(Person,function(name,age){this.age=age;});Boy.prototype.sex="M";varPeter=newBoy("Peter",13);console.log(Peter.sex);//"M"console.log(Peter.name);//"Peter"console.log(Peter.age);//13操作DOM节点
有时,我们可能需要互换两个不同的元素的属性或类名。下面的代码以此为目标,展示了sethandler的使用场景。
letview=newProxy({selected:null},{set:function(obj,prop,newval){letoldval=obj[prop];if(prop===selected){if(oldval){oldval.setAttribute(aria-selected,false);}if(newval){newval.setAttribute(aria-selected,true);}}//默认行为是存储被传入setter函数的属性值obj[prop]=newval;//表示操作成功returntrue;}});leti1=view.selected=document.getElementById(item-1);console.log(i1.getAttribute(aria-selected));//trueleti2=view.selected=document.getElementById(item-2);console.log(i1.getAttribute(aria-selected));//falseconsole.log(i2.getAttribute(aria-selected));//true值修正及附加属性
以下products代理会计算传值并根据需要转换为数组。这个代理对象同时支持一个叫做latestBrowser的附加属性,这个属性可以同时作为getter和setter。
letproducts=newProxy({browsers:[InternetExplorer,Netscape]},{get:function(obj,prop){//附加一个属性if(prop===latestBrowser){returnobj.browsers[obj.browsers.length-1];}//默认行为是返回属性值returnobj[prop];},set:function(obj,prop,value){//附加属性if(prop===latestBrowser){obj.browsers.push(value);return;}//如果不是数组,则进行转换if(typeofvalue===string){value=[value];}//默认行为是保存属性值obj[prop]=value;//表示成功returntrue;}});console.log(products.browsers);//[InternetExplorer,Netscape]products.browsers=Firefox;//如果不小心传入了一个字符串console.log(products.browsers);//[Firefox]-也没问题,得到的依旧是一个数组products.latestBrowser=Chrome;console.log(products.browsers);//[Firefox,Chrome]console.log(products.latestBrowser);//Chrome通过属性查找数组中的特定对象
以下代理为数组扩展了一些实用工具。如你所见,通过Proxy,我们可以灵活地“定义”属性,而不需要使用Object.defineProperties方法。以下例子可以用于通过单元格来查找表格中的一行。在这种情况下,target是table.rows。
letproducts=newProxy([{name:Firefox,type:browser},{name:SeaMonkey,type:browser},{name:Thunderbird,type:mailer}],{get:function(obj,prop){//默认行为是返回属性值,prop?通常是一个整数if(propinobj){returnobj[prop];}//获取products的number;它是products.length的别名if(prop===number){returnobj.length;}letresult,types={};for(letproductofobj){if(product.name===prop){result=product;}if(types[product.type]){types[product.type].push(product);}else{types[product.type]=[product];}}//通过name获取productif(result){returnresult;}//通过type获取productsif(propintypes){returntypes[prop];}//获取producttypeif(prop===types){returnObject.keys(types);}returnundefined;}});console.log(products[0]);//{name:Firefox,type:browser}console.log(products[Firefox]);//{name:Firefox,type:browser}console.log(products[Chrome]);//undefinedconsole.log(products.browser);//[{name:Firefox,type:browser},{name:SeaMonkey,type:browser}]console.log(products.types);//[browser,mailer]console.log(products.number);//3一个完整的traps列表示例
出于教学目的,这里为了创建一个完整的traps列表示例,我们将尝试代理化一个非原生对象,这特别适用于这类操作:由发布在document.cookie页面上的“小型框架”创建的docCookies全局对象。
/*vardocCookies=...getthe"docCookies"objecthere:
最近更新
推荐文章