<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>

          【JS】827- 徹底搞懂Object.defineProperty

          共 8796字,需瀏覽 18分鐘

           ·

          2021-01-06 01:46


          • 本文作者:聽風是風

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

          前言

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

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

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

          例如:

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

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

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

          從零認識defineProperty

          基本用法與屬性

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

          Object.defineProperty方法用于在對象上定義一個新屬性,或者修改對象現(xiàn)有屬性,并返回此對象。注意,請通過Object構造器調用此方法,而不是對象實例。

          方法基本語法如下:

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

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

          //?添加屬性
          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是我們要添加/修改屬性的對象,prop是我們希望添加/修改的屬性名,而descriptor是我們添加/修改屬性的具體描述,descriptor包含屬性較多,我們展開說。

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

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

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

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

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

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

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

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

          注意,如果在嚴格模式下,修改writable屬性為false的對象屬性會報錯。但如果將上述代碼的writalbe改為false就可以隨意更改了。

          而在MDN中關于writable屬性的描述為:

          當該屬性的?writable?鍵值為?true?時,屬性的值,也就是上面的?value,才能被賦值運算符改變。

          這里我做個知識補充,讓MDN這句描述更為準確。

          在面試時有時候會被問到,const聲明的變量是否可修改,準確來說可以改,分兩種情況:

          //?值為基本類型
          const?a?=?1;
          a?=?2;//?報錯

          //?值為復雜類型
          const?b?=?[1];
          b?=?[1,2];//?報錯

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

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

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

          而這個特性對于writable也是適用的,比如下面這個例子:

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

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

          descriptor中的存取描述符

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

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

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

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

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

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

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

          let?o?=?{};
          o.name?=?'聽風是風';
          o.name;?//?'聽風是風'

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

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

          OK,到這里我們順利學習了存取描述符settergetter

          descriptor中的共有屬性

          最后,讓我們了解剩余兩個屬性configurableenumerable

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

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

          通俗點來說,上面例子中的兩個屬性還是可以遍歷訪問的,但如果我們設置enumerable為false,就會變成這樣:

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

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

          先說刪除,看個例子:

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

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

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

          刪除好說,我們來看看它對于其它屬性的影響,看個例子:

          var?o?=?{};
          Object.defineProperty(o,?'name',?{
          ????get()?{
          ????????return?'聽風是風';
          ????},
          ????configurable:?false
          });
          //?報錯,嘗試通過再配置修改name的configurable失敗,因為已經(jīng)定義過了configurable
          Object.defineProperty(o,?'name',?{
          ????configurable:?true
          });

          //報錯,嘗試修改name的enumerable為true,失敗,因為未定義默認為false
          Object.defineProperty(o,?'name',?{
          ????enumerable:?true
          });

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

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

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

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

          其它注意點

          那么到這里,我們把descriptor中所有屬性都介紹完了,在使用中有幾點需要強調,這里再匯總一下。

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

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

          這個例子就會報錯,其實不難理解,存取方法就是用來定義屬性值的,value也是用來定義值的,同時定義程序也不知道該以哪個為準了,所以用了value/writable其一,就不能用get/set了;不過configurableenumerable這兩個屬性可以與上面兩種屬性任意搭配。

          我們在前面已經(jīng)說了各個屬性是有默認值的,所以在用Object.defineProperty()時某個屬性沒定義不是代表沒用這條屬性,而是會用這條屬性的默認值。

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

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

          同理,以下代碼也對等:

          var?o?=?{};
          o.name?=?'聽風是風';

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

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

          關于屬性分類與默認值,如下表:


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

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

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

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

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

          function?Person()?{
          ????//?初始化年齡
          ????let?age;
          ????Object.defineProperty(this,?"age",?{
          ????????get()?{
          ????????????let?ageRange?=?[41,?20,?0],
          ????????????????level?=?['老年',?'中年',?'少年'];
          ????????????for?(let?i?=?0;?i?????????????????//?根據(jù)年紀大小返回對應范圍
          ????????????????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);?//?'老年'

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

          為什么我不用ES6的class類來實現(xiàn)上面的操作了,因為公司不允許使用ES6,去年學的關于類好多都忘記了...整理這篇文章也花了好長時間,腦袋有點沉,這個改寫就留給各位強大的網(wǎng)友吧。

          那么到這里,關于Object.defineProperty的介紹就結束了。

          補充

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

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

          那么知道了這一點,我們來按照出題人的本意來分別實現(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ù)年紀大小返回對應范圍
          ????????????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)?{
          ????????//?這里就等同于我的第一個實現(xiàn)里面let?age,是一個中間變量
          ????????this.age_?=?undefined;
          ????}
          ????//?ES6中,原型方法可直接定義在類中
          ????get?age()?{
          ????????let?ageRange?=?[41,?20,?0],
          ????????????level?=?['老年',?'中年',?'少年'];
          ????????for?(let?i?=?0;?i?????????????//?根據(jù)年紀大小返回對應范圍
          ????????????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,這樣又有一部分知識串起來了,賊開心!

          1. JavaScript 重溫系列(22篇全)
          2. ECMAScript 重溫系列(10篇全)
          3. JavaScript設計模式 重溫系列(9篇全)
          4.?正則 / 框架 / 算法等 重溫系列(16篇全)
          5.?Webpack4 入門(上)||?Webpack4 入門(下)
          6.?MobX 入門(上)?||??MobX 入門(下)
          7. 80+篇原創(chuàng)系列匯總

          回復“加群”與大佬們一起交流學習~

          點擊“閱讀原文”查看 100+ 篇原創(chuàng)文章

          瀏覽 21
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          <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>
                  西西西444www无码视 | 无码黄片免费在线观看 | 麻豆影院久久久 | 免费成人视频久久 | 欧美十日本十国产 |