<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          徹底搞懂Object.defineProperty

          共 8591字,需瀏覽 18分鐘

           ·

          2021-01-06 20:34


          • 本文作者:聽風(fēng)是風(fēng)

          • 本文鏈接:https://www.cnblogs.com/echolun/p/13121214.html

          前言

          早在大半年前,掘金某位用戶分享的面試題整理中有一題,簡(jiǎn)述let與const區(qū)別,你能自己模擬實(shí)現(xiàn)它們嗎?,題目意思大概如此,時(shí)間久遠(yuǎn)我也很難找到那篇文章,當(dāng)時(shí)看到此題對(duì)于const實(shí)現(xiàn)我的想法就是有個(gè)writable屬性可以定義值是否可以修改,不過也只是腦中一閃,并未細(xì)究。

          半個(gè)月前,前前同事發(fā)了一份深圳某公司的筆試題我,整體題目不難(不難是指每題都知道考的什么知識(shí)點(diǎn),腦中都能想到該用什么去解決,但知識(shí)不一定很精通),其中有一道手寫編程題,題目描述如下:

          使用function和class兩種方案,寫一個(gè)類Person,可以設(shè)置年齡為正整數(shù),年齡區(qū)段返回少年(0-20),中年(21-40)以及老年(其他)。

          例如:

          Person.age?=?1;
          console.log(Person.age);//?'少年'

          在我印象里JavaScript對(duì)象是可以用gettersetter來解決這個(gè)問題的,存一個(gè)數(shù)字進(jìn)去,取的時(shí)候根據(jù)數(shù)字范圍返回對(duì)應(yīng)年齡段,我只是說了我的想法,并未真正去實(shí)現(xiàn)它,因?yàn)槲覍?duì)于這兩個(gè)方法也只是有點(diǎn)印象而已。

          昨天,在我通讀vue文檔過程中,一篇名為深入響應(yīng)式原理吸引了我的注意,文中簡(jiǎn)述了vue數(shù)據(jù)響應(yīng)式的原理,以及在操作數(shù)組與對(duì)象時(shí)需要注意的點(diǎn),在實(shí)現(xiàn)上vue也使用了Object.defineProperty方法,聯(lián)想到vue計(jì)算屬性的gettersetter,我想是時(shí)候弄懂這個(gè)API了,那么請(qǐng)各位跟隨我的腳步,好好認(rèn)識(shí)這個(gè)在JavaScript中高頻出現(xiàn)的API,本文開始。

          從零認(rèn)識(shí)defineProperty

          基本用法與屬性

          讓我們從基本概念說起,這里引用MDN解釋:

          Object.defineProperty方法用于在對(duì)象上定義一個(gè)新屬性,或者修改對(duì)象現(xiàn)有屬性,并返回此對(duì)象。注意,請(qǐng)通過Object構(gòu)造器調(diào)用此方法,而不是對(duì)象實(shí)例。

          方法基本語法如下:

          Object.defineProperty(obj,?prop,?descriptor)

          OK,結(jié)合基本用法與概念,我們來試試添加屬性與修改屬性。

          //?添加屬性
          let?o?=?{};
          Object.defineProperty(o,?'name',?{value:'echo'});
          o.name;//?'echo'

          //?修改現(xiàn)有屬性
          o.age?=?27;
          //?重返18歲
          Object.defineProperty(o,?'age',?{value:18});
          o.age;//?18

          通過上面的例子演示我們可知,語法中的obj是我們要添加/修改屬性的對(duì)象,prop是我們希望添加/修改的屬性名,而descriptor是我們添加/修改屬性的具體描述,descriptor包含屬性較多,我們展開說。

          descriptor中的數(shù)據(jù)描述符

          Object.defineProperty方法中的descriptor屬性繁多,所以它也非常強(qiáng)大,我們之前說的數(shù)據(jù)劫持,數(shù)據(jù)是否可寫,是否可刪除,是否可枚舉都在這個(gè)descriptor中定義。在介紹每個(gè)屬性前,我們還得引入一個(gè)新概念,即:

          對(duì)象里目前存在的屬性描述符有兩種主要形式:數(shù)據(jù)描述符存取描述符數(shù)據(jù)描述符是一個(gè)具有值的屬性,該值可以是可寫的,也可以是不可寫的。存取描述符由 getter 函數(shù)和 setter 函數(shù)所描述的屬性。一個(gè)描述符只能是這兩者其中之一,不能同時(shí)是兩者。

          descriptor中包含的屬性也分為了兩類,怕大家弄混淆,這里我用圖分個(gè)類:

          descriptor中屬性包含6個(gè)(參考上圖),我將其分為了3類,數(shù)據(jù)描述符類(value,writable),存取描述符類(get,set),以及能與數(shù)據(jù)描述符或者存取描述符共存的共有屬性(configurable,enumerable)。

          讓我們一一介紹它們,在對(duì)象添加屬性以及修改屬性時(shí)已經(jīng)展示過value屬性的作用了,所以這里直接從writable開始。

          writable是一個(gè)布爾值,若不定義默認(rèn)為false,表示此條屬性只可讀,不可修改,舉個(gè)例子:

          let?o?=?{};
          Object.defineProperty(o,?'name',?{
          ????value:?'聽風(fēng)是風(fēng)',
          ????writable:?false
          });
          //?嘗試修改name屬性
          o.name?=?'時(shí)間跳躍';
          //?再次讀取,結(jié)果并未修改成功
          o.name;//?聽風(fēng)是風(fēng)

          注意,如果在嚴(yán)格模式下,修改writable屬性為false的對(duì)象屬性會(huì)報(bào)錯(cuò)。但如果將上述代碼的writalbe改為false就可以隨意更改了。

          而在MDN中關(guān)于writable屬性的描述為:

          當(dāng)該屬性的?writable?鍵值為?true?時(shí),屬性的值,也就是上面的?value,才能被賦值運(yùn)算符改變。

          這里我做個(gè)知識(shí)補(bǔ)充,讓MDN這句描述更為準(zhǔn)確。

          在面試時(shí)有時(shí)候會(huì)被問到,const聲明的變量是否可修改,準(zhǔn)確來說可以改,分兩種情況:

          //?值為基本類型
          const?a?=?1;
          a?=?2;//?報(bào)錯(cuò)

          //?值為復(fù)雜類型
          const?b?=?[1];
          b?=?[1,2];//?報(bào)錯(cuò)

          const?c?=?[1];
          c[0]?=?0;
          c;//?[0]

          如果我們const聲明變量賦值是基本類型,只要修改值一定報(bào)錯(cuò);如果值是引用類型,比如值是一個(gè)數(shù)組,當(dāng)我們直接使用賦值運(yùn)算符整個(gè)替換數(shù)組還是會(huì)報(bào)錯(cuò),但如果我們不是整個(gè)替換數(shù)組而是修改數(shù)組中某個(gè)元素可以發(fā)現(xiàn)并不會(huì)報(bào)錯(cuò)。

          這是因?yàn)閷?duì)于引用數(shù)據(jù)類型而言,變量保存的是數(shù)據(jù)存放的引用地址,比如b的例子,原本指向是[1]的地址,后面直接要把地址改成數(shù)組[1,2]的地址,這很明顯是不允許的,所以報(bào)錯(cuò)了。但在c的例子中,我們只是把c地址指向的數(shù)組中的一位元素改變了,并未修改地址,這對(duì)于const是允許的。

          而這個(gè)特性對(duì)于writable也是適用的,比如下面這個(gè)例子:

          let?o?=?{};
          Object.defineProperty(o,?'age',?{
          ????value:?[27],
          ????writable:?false
          });
          //?嘗試修改name屬性
          o.age[0]?=?18;
          //?再次讀取,修改成功
          o.age;?//?18

          你看,修改成功了,所以針對(duì)MDNwritable為true才能被賦值運(yùn)算符改變這句話不一定正確,比如上個(gè)例子我們就是用賦值運(yùn)算符修改了數(shù)組索引為0的一項(xiàng)的值,具體問題具體看待,這里做個(gè)補(bǔ)充。

          descriptor中的存取描述符

          OK,我們介紹了descriptor中的數(shù)據(jù)描述符相關(guān)的vaulewritbale,接著聊聊有趣的存取描述符,也就是在vue中也出現(xiàn)過getter、setter方法。

          我們知道,JavaScript中對(duì)象賦值與取值非常方便,有如下兩種方式:

          let?o?=?{};
          //?通過.賦值取值
          o.name?=?'echo';
          //通過[]賦值取值,這種常用于key為變量情況
          o['age']?=?27;

          一個(gè)很直觀的感受就是,對(duì)象賦值就是種瓜得瓜種豆得豆,我們給對(duì)象賦予了什么,獲取的就是什么。那大家有沒有想過這種情況,賦值時(shí)我提供1,但取值我希望是2。巧了,這種情況我們就可以使用Object.defineProperty()中的存取描述符來解決這個(gè)需求。說直白點(diǎn),存取描述符給了我們賦值/取值時(shí)數(shù)據(jù)劫持的機(jī)會(huì),也就就是在賦值與取值時(shí)能自定義做一些操作,

          getter函數(shù)在獲取屬性值時(shí)觸發(fā),注意,是你為某個(gè)屬性添加了getter在獲取這個(gè)屬性才會(huì)觸發(fā),如果未定義則為undefined,該函數(shù)的返回值將作為你訪問的屬性值。

          setter函數(shù)在設(shè)置屬性時(shí)觸發(fā),同理你得為這個(gè)屬性提前定義這個(gè)方法才行,設(shè)置的值將作為參數(shù)傳入到setter函數(shù)中,在這里我們可以加工數(shù)據(jù),若未定義此方法默認(rèn)也是undefined。

          OK,讓我們用gettersetter模擬最常見的對(duì)象賦值與取值,看個(gè)例子:

          let?o?=?{};
          o.name?=?'聽風(fēng)是風(fēng)';
          o.name;?//?'聽風(fēng)是風(fēng)'

          //使用get?set模擬賦值取值操作
          let?age;
          Object.defineProperty(o,?'age',?{
          ????get()?{
          ????????//?直接返回age
          ????????return?age;
          ????},
          ????set(val)?{
          ????????//?賦值時(shí)觸發(fā),將值賦予變量age
          ????????age?=?val;
          ????}
          });
          o.age?=?18;
          o.age;?//?18

          在上面例子模擬中,只要為o賦值setter就會(huì)觸發(fā),并將值賦予給age,那么在讀取值getter直接返回變量age即可。

          OK,到這里我們順利學(xué)習(xí)了存取描述符settergetter

          descriptor中的共有屬性

          最后,讓我們了解剩余兩個(gè)屬性configurableenumerable

          enumerable值類型為Boolean,表示該屬性是否可被枚舉,啥意思?我們知道對(duì)象中有個(gè)方法Object.keys()用于獲取對(duì)象可枚舉屬性,比如:

          let?o?=?{
          ????name:?'聽風(fēng)是風(fēng)',
          ????age:?27
          };
          Object.keys(o);?//?['name','age']

          通俗點(diǎn)來說,上面例子中的兩個(gè)屬性還是可以遍歷訪問的,但如果我們?cè)O(shè)置enumerable為false,就會(huì)變成這樣:

          let?o?=?{
          ????name:?'聽風(fēng)是風(fēng)'
          };
          Object.defineProperty(o,?'age',?{
          ????value:?27,
          ????enumerable:?false
          });
          //?無法獲取keys
          Object.keys(o);?//?['name']
          //?無法遍歷訪問
          for?(let?i?in?o)?{
          ????console.log(i);?//?'name'
          };

          configurable的值也是Boolean,默認(rèn)是false,configurable?特性表示對(duì)象的屬性是否可以被刪除,以及除?value?和?writable?特性外的其他特性是否可以被修改。

          先說刪除,看個(gè)例子:

          let?o?=?{
          ????name:?'聽風(fēng)是風(fēng)'
          };
          Object.defineProperty(o,?'age',?{
          ????value:?27,
          ????configurable:?false
          });

          delete?o.name;//true
          delete?o.age;//false

          o.name;//undefined
          o.age;//18

          刪除好說,我們來看看它對(duì)于其它屬性的影響,看個(gè)例子:

          var?o?=?{};
          Object.defineProperty(o,?'name',?{
          ????get()?{
          ????????return?'聽風(fēng)是風(fēng)';
          ????},
          ????configurable:?false
          });
          //?報(bào)錯(cuò),嘗試通過再配置修改name的configurable失敗,因?yàn)橐呀?jīng)定義過了configurable
          Object.defineProperty(o,?'name',?{
          ????configurable:?true
          });

          //報(bào)錯(cuò),嘗試修改name的enumerable為true,失敗,因?yàn)槲炊x默認(rèn)為false
          Object.defineProperty(o,?'name',?{
          ????enumerable:?true
          });

          //報(bào)錯(cuò),嘗試新增set函數(shù),失敗,一開始沒定義set默認(rèn)為undefined
          Object.defineProperty(o,?'name',?{
          ????set()?{}
          });

          //嘗試再定義get,報(bào)錯(cuò),已經(jīng)定義過了
          Object.defineProperty(o,?'name',?{
          ????get()?{
          ????????return?1;
          ????}
          });

          //?嘗試添加數(shù)據(jù)描述符中的vaule,報(bào)錯(cuò),數(shù)據(jù)描述符無法與存取描述符共存
          Object.defineProperty(o,?'name',?{
          ????value:?12
          });

          由于前面我們說了,未定義的屬性雖然沒用代碼寫出來,但它們其實(shí)都有了默認(rèn)值,當(dāng)configurable為false時(shí),這些屬性都無法被重新定義以及修改。

          其它注意點(diǎn)

          那么到這里,我們把descriptor中所有屬性都介紹完了,在使用中有幾點(diǎn)需要強(qiáng)調(diào),這里再匯總一下。

          前面概念已經(jīng)提出對(duì)象屬性描述符要么是數(shù)據(jù)描述符(value,writable),要么是存取描述符(get,set),不應(yīng)該同時(shí)存在兩者描述符。

          var?o?=?{};
          Object.defineProperty(o,?'name',?{
          ????value:?'時(shí)間跳躍',
          ????get()?{
          ????????return?'聽風(fēng)是風(fēng)';
          ????}
          });

          這個(gè)例子就會(huì)報(bào)錯(cuò),其實(shí)不難理解,存取方法就是用來定義屬性值的,value也是用來定義值的,同時(shí)定義程序也不知道該以哪個(gè)為準(zhǔn)了,所以用了value/writable其一,就不能用get/set了;不過configurableenumerable這兩個(gè)屬性可以與上面兩種屬性任意搭配。

          我們?cè)谇懊嬉呀?jīng)說了各個(gè)屬性是有默認(rèn)值的,所以在用Object.defineProperty()時(shí)某個(gè)屬性沒定義不是代表沒用這條屬性,而是會(huì)用這條屬性的默認(rèn)值。

          let?o?=?{};
          Object.defineProperty(o,?'name',?{
          ????value:?'時(shí)間跳躍'
          });

          //等同于
          Object.defineProperty(o,?'name',?{
          ????value:?'時(shí)間跳躍',
          ????writable:?false,
          ????enumerable:?false,
          ????configurable:?false
          });

          同理,以下代碼也對(duì)等:

          var?o?=?{};
          o.name?=?'聽風(fēng)是風(fēng)';

          //等同于
          Object.defineProperty(o,?'name',?{
          ????value:?'聽風(fēng)是風(fēng)',
          ????writable:?true,
          ????enumerable:?true,
          ????configurable:?true
          });

          //等同于
          let?name?=?'聽風(fēng)是風(fēng)';
          Object.defineProperty(o,?'name',?{
          ????get()?{
          ????????return?name;
          ????},
          ????set(val)?{
          ????????name?=?val;
          ????},
          ????enumerable:?true,
          ????configurable:?true
          });

          關(guān)于屬性分類與默認(rèn)值,如下表:


          configurableenumerablevaluewritablegetset
          數(shù)據(jù)描述符可以可以可以可以不可以不可以
          存取描述符可以可以不可以不可以可以可以
          默認(rèn)值falsefalsefalsefalseundefinedundefined

          現(xiàn)學(xué)現(xiàn)用,趁熱打鐵

          那么到這里,我們?cè)敿?xì)介紹了Object.defineProperty相關(guān)屬性與用法,趁熱打鐵,我們活用它來解決一些問題。原本我想通過模擬vue數(shù)據(jù)雙向綁定,模擬const以及解決文章開頭面試題,但礙于文章篇幅確實(shí)過長了,const模擬大家感興趣可自行百度,vue數(shù)據(jù)雙向綁定我會(huì)另起一篇文章,所以這里就來解決文章開頭的題目好了。

          我們提取題目細(xì)節(jié),年齡只接受正整數(shù)(在set中判斷),畢竟沒人是負(fù)年齡,其次對(duì)應(yīng)范圍有對(duì)應(yīng)的年齡段,根據(jù)年齡返回對(duì)應(yīng)年齡段即可(在get中操作);

          這里直接上function的實(shí)現(xiàn):

          function?Person()?{
          ????//?初始化年齡
          ????let?age;
          ????Object.defineProperty(this,?"age",?{
          ????????get()?{
          ????????????let?ageRange?=?[41,?20,?0],
          ????????????????level?=?['老年',?'中年',?'少年'];
          ????????????for?(let?i?=?0;?i?????????????????//?根據(jù)年紀(jì)大小返回對(duì)應(yīng)范圍
          ????????????????if?(age?>=?ageRange[i])?{
          ????????????????????return?level[i];
          ????????????????};
          ????????????};
          ????????},
          ????????set(val)?{
          ????????????//?年齡只保存正整數(shù)
          ????????????val?>=?0???age?=?val?:?null;
          ????????}
          ????});
          };

          let?p?=?new?Person();
          p.age?=?1;
          console.log(p.age);?//?'少年'
          p.age?=?39;
          console.log(p.age);?//?'中年'
          p.age?=?41;
          console.log(p.age);?//?'老年'

          值得一提的是,實(shí)現(xiàn)代碼中我們將需要年齡與相關(guān)返回值配置成了數(shù)組,而非常理上的if...else if...,這樣做的好處是即便修改年齡或者增加年齡范圍,我們要做的也僅僅是修改數(shù)組配置即可,而不需要對(duì)邏輯層中添加更多的if...else。更多條件判斷優(yōu)雅寫法歡迎閱讀博主這篇文章?提升代碼幸福度,五個(gè)技巧減少js開發(fā)中的if else語句

          為什么我不用ES6的class類來實(shí)現(xiàn)上面的操作了,因?yàn)楣静辉试S使用ES6,去年學(xué)的關(guān)于類好多都忘記了...整理這篇文章也花了好長時(shí)間,腦袋有點(diǎn)沉,這個(gè)改寫就留給各位強(qiáng)大的網(wǎng)友吧。

          那么到這里,關(guān)于Object.defineProperty的介紹就結(jié)束了。

          補(bǔ)充

          關(guān)于上面這道題,考察的雖然是Object.definedProperty的getter與setter,不過出題人的本意不是希望這么用的,任何對(duì)象在定義時(shí)候可以添加get,set方法,比如:

          let?p?=?{
          ????age_:?27,
          ????name:?'echo',
          ????get?age()?{
          ????????return?this.age_;
          ????},
          ????get?name()?{
          ????????return?'聽風(fēng)是風(fēng)'
          ????}
          };
          p.name;?//?聽風(fēng)是風(fēng)
          p.age;?//?27

          那么知道了這一點(diǎn),我們來按照出題人的本意來分別實(shí)現(xiàn)上面的題目,首先是function情況:

          function?Person()?{
          ????//?初始化年齡
          ????this.age_?=?undefined;

          };
          //?在函數(shù)原型上定義age的get,set方法
          Person.prototype?=?{
          ????get?age()?{
          ????????let?ageRange?=?[41,?20,?0],
          ????????????level?=?['老年',?'中年',?'少年'];
          ????????for?(let?i?=?0;?i?????????????//?根據(jù)年紀(jì)大小返回對(duì)應(yīng)范圍
          ????????????if?(this.age_?>=?ageRange[i])?{
          ????????????????return?level[i];
          ????????????};
          ????????};
          ????},
          ????set?age(val)?{
          ????????//?年齡只保存正整數(shù)
          ????????val?>=?0???this.age_?=?val?:?null;
          ????}
          }

          let?p?=?new?Person();
          p.age?=?1;

          其次是ES6的class類:

          class?Person?{
          ????constructor(age)?{
          ????????//?這里就等同于我的第一個(gè)實(shí)現(xiàn)里面let?age,是一個(gè)中間變量
          ????????this.age_?=?undefined;
          ????}
          ????//?ES6中,原型方法可直接定義在類中
          ????get?age()?{
          ????????let?ageRange?=?[41,?20,?0],
          ????????????level?=?['老年',?'中年',?'少年'];
          ????????for?(let?i?=?0;?i?????????????//?根據(jù)年紀(jì)大小返回對(duì)應(yīng)范圍
          ????????????if?(this.age_?>=?ageRange[i])?{
          ????????????????return?level[i];
          ????????????};
          ????????};
          ????}
          ????set?age(age)?{
          ????????age?>=?0???this.age_?=?age?:?null;
          ????}

          };

          var?p?=?new?Person();
          p.age?=?1;
          console.log(p.age);?//少年

          OK,這樣又有一部分知識(shí)串起來了,賊開心!

          點(diǎn)個(gè)『在看』支持下?


          瀏覽 34
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評(píng)論
          圖片
          表情
          推薦
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  爆操小姐姐 | 超碰超碰成人 | 少妇综合精品导航 | 撸一撸成人网站 | 日本a免费看 |