徹底搞懂 Object.defineProperty
本文作者:聽(tīng)風(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屬性可以定義值是否可以修改,不過(guò)也只是腦中一閃,并未細(xì)究。
半個(gè)月前,前前同事發(fā)了一份深圳某公司的筆試題我,整體題目不難(不難是指每題都知道考的什么知識(shí)點(diǎn),腦中都能想到該用什么去解決,但知識(shí)不一定很精通),其中有一道手寫(xiě)編程題,題目描述如下:
使用function和class兩種方案,寫(xiě)一個(gè)類Person,可以設(shè)置年齡為正整數(shù),年齡區(qū)段返回少年(0-20),中年(21-40)以及老年(其他)。
例如:
Person.age?=?1;
console.log(Person.age);//?'少年'
在我印象里JavaScript對(duì)象是可以用getter與setter來(lái)解決這個(gè)問(wèn)題的,存一個(gè)數(shù)字進(jìn)去,取的時(shí)候根據(jù)數(shù)字范圍返回對(duì)應(yīng)年齡段,我只是說(shuō)了我的想法,并未真正去實(shí)現(xiàn)它,因?yàn)槲覍?duì)于這兩個(gè)方法也只是有點(diǎn)印象而已。
昨天,在我通讀vue文檔過(guò)程中,一篇名為深入響應(yīng)式原理吸引了我的注意,文中簡(jiǎn)述了vue數(shù)據(jù)響應(yīng)式的原理,以及在操作數(shù)組與對(duì)象時(shí)需要注意的點(diǎn),在實(shí)現(xiàn)上vue也使用了Object.defineProperty方法,聯(lián)想到vue計(jì)算屬性的getter與setter,我想是時(shí)候弄懂這個(gè)API了,那么請(qǐng)各位跟隨我的腳步,好好認(rèn)識(shí)這個(gè)在JavaScript中高頻出現(xiàn)的API,本文開(kāi)始。
從零認(rèn)識(shí)defineProperty
基本用法與屬性
讓我們從基本概念說(shuō)起,這里引用MDN解釋:
Object.defineProperty方法用于在對(duì)象上定義一個(gè)新屬性,或者修改對(duì)象現(xiàn)有屬性,并返回此對(duì)象。注意,請(qǐng)通過(guò)Object構(gòu)造器調(diào)用此方法,而不是對(duì)象實(shí)例。
方法基本語(yǔ)法如下:
Object.defineProperty(obj,?prop,?descriptor)
OK,結(jié)合基本用法與概念,我們來(lái)試試添加屬性與修改屬性。
//?添加屬性
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
通過(guò)上面的例子演示我們可知,語(yǔ)法中的obj是我們要添加/修改屬性的對(duì)象,prop是我們希望添加/修改的屬性名,而descriptor是我們添加/修改屬性的具體描述,descriptor包含屬性較多,我們展開(kāi)說(shuō)。
descriptor中的數(shù)據(jù)描述符
Object.defineProperty方法中的descriptor屬性繁多,所以它也非常強(qiáng)大,我們之前說(shuō)的數(shù)據(jù)劫持,數(shù)據(jù)是否可寫(xiě),是否可刪除,是否可枚舉都在這個(gè)descriptor中定義。在介紹每個(gè)屬性前,我們還得引入一個(gè)新概念,即:
對(duì)象里目前存在的屬性描述符有兩種主要形式:數(shù)據(jù)描述符和存取描述符。數(shù)據(jù)描述符是一個(gè)具有值的屬性,該值可以是可寫(xiě)的,也可以是不可寫(xiě)的。存取描述符是由 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)展示過(guò)value屬性的作用了,所以這里直接從writable開(kāi)始。
writable是一個(gè)布爾值,若不定義默認(rèn)為false,表示此條屬性只可讀,不可修改,舉個(gè)例子:
let?o?=?{};
Object.defineProperty(o,?'name',?{
????value:?'聽(tīng)風(fēng)是風(fēng)',
????writable:?false
});
//?嘗試修改name屬性
o.name?=?'時(shí)間跳躍';
//?再次讀取,結(jié)果并未修改成功
o.name;//?聽(tīng)風(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ì)被問(wèn)到,const聲明的變量是否可修改,準(zhǔn)確來(lái)說(shuō)可以改,分兩種情況:
//?值為基本類型
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)的值,具體問(wèn)題具體看待,這里做個(gè)補(bǔ)充。
descriptor中的存取描述符
OK,我們介紹了descriptor中的數(shù)據(jù)描述符相關(guān)的vaule與writbale,接著聊聊有趣的存取描述符,也就是在vue中也出現(xiàn)過(guò)getter、setter方法。
我們知道,JavaScript中對(duì)象賦值與取值非常方便,有如下兩種方式:
let?o?=?{};
//?通過(guò).賦值取值
o.name?=?'echo';
//通過(guò)[]賦值取值,這種常用于key為變量情況
o['age']?=?27;
一個(gè)很直觀的感受就是,對(duì)象賦值就是種瓜得瓜種豆得豆,我們給對(duì)象賦予了什么,獲取的就是什么。那大家有沒(méi)有想過(guò)這種情況,賦值時(shí)我提供1,但取值我希望是2。巧了,這種情況我們就可以使用Object.defineProperty()中的存取描述符來(lái)解決這個(gè)需求。說(shuō)直白點(diǎn),存取描述符給了我們賦值/取值時(shí)數(shù)據(jù)劫持的機(jī)會(huì),也就就是在賦值與取值時(shí)能自定義做一些操作,
getter函數(shù)在獲取屬性值時(shí)觸發(fā),注意,是你為某個(gè)屬性添加了getter在獲取這個(gè)屬性才會(huì)觸發(fā),如果未定義則為undefined,該函數(shù)的返回值將作為你訪問(wèn)的屬性值。
setter函數(shù)在設(shè)置屬性時(shí)觸發(fā),同理你得為這個(gè)屬性提前定義這個(gè)方法才行,設(shè)置的值將作為參數(shù)傳入到setter函數(shù)中,在這里我們可以加工數(shù)據(jù),若未定義此方法默認(rèn)也是undefined。
OK,讓我們用getter與setter模擬最常見(jiàn)的對(duì)象賦值與取值,看個(gè)例子:
let?o?=?{};
o.name?=?'聽(tīng)風(fēng)是風(fēng)';
o.name;?//?'聽(tīng)風(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í)了存取描述符setter與getter。
descriptor中的共有屬性
最后,讓我們了解剩余兩個(gè)屬性configurable與enumerable。
enumerable值類型為Boolean,表示該屬性是否可被枚舉,啥意思?我們知道對(duì)象中有個(gè)方法Object.keys()用于獲取對(duì)象可枚舉屬性,比如:
let?o?=?{
????name:?'聽(tīng)風(fēng)是風(fēng)',
????age:?27
};
Object.keys(o);?//?['name','age']
通俗點(diǎn)來(lái)說(shuō),上面例子中的兩個(gè)屬性還是可以遍歷訪問(wèn)的,但如果我們?cè)O(shè)置enumerable為false,就會(huì)變成這樣:
let?o?=?{
????name:?'聽(tīng)風(fēng)是風(fēng)'
};
Object.defineProperty(o,?'age',?{
????value:?27,
????enumerable:?false
});
//?無(wú)法獲取keys
Object.keys(o);?//?['name']
//?無(wú)法遍歷訪問(wèn)
for?(let?i?in?o)?{
????console.log(i);?//?'name'
};
configurable的值也是Boolean,默認(rèn)是false,configurable?特性表示對(duì)象的屬性是否可以被刪除,以及除?value?和?writable?特性外的其他特性是否可以被修改。
先說(shuō)刪除,看個(gè)例子:
let?o?=?{
????name:?'聽(tīng)風(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
刪除好說(shuō),我們來(lái)看看它對(duì)于其它屬性的影響,看個(gè)例子:
var?o?=?{};
Object.defineProperty(o,?'name',?{
????get()?{
????????return?'聽(tīng)風(fēng)是風(fēng)';
????},
????configurable:?false
});
//?報(bào)錯(cuò),嘗試通過(guò)再配置修改name的configurable失敗,因?yàn)橐呀?jīng)定義過(guò)了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ù),失敗,一開(kāi)始沒(méi)定義set默認(rèn)為undefined
Object.defineProperty(o,?'name',?{
????set()?{}
});
//嘗試再定義get,報(bào)錯(cuò),已經(jīng)定義過(guò)了
Object.defineProperty(o,?'name',?{
????get()?{
????????return?1;
????}
});
//?嘗試添加數(shù)據(jù)描述符中的vaule,報(bào)錯(cuò),數(shù)據(jù)描述符無(wú)法與存取描述符共存
Object.defineProperty(o,?'name',?{
????value:?12
});
由于前面我們說(shuō)了,未定義的屬性雖然沒(méi)用代碼寫(xiě)出來(lái),但它們其實(shí)都有了默認(rèn)值,當(dāng)configurable為false時(shí),這些屬性都無(wú)法被重新定義以及修改。
其它注意點(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?'聽(tīng)風(fēng)是風(fēng)';
????}
});
這個(gè)例子就會(huì)報(bào)錯(cuò),其實(shí)不難理解,存取方法就是用來(lái)定義屬性值的,value也是用來(lái)定義值的,同時(shí)定義程序也不知道該以哪個(gè)為準(zhǔn)了,所以用了value/writable其一,就不能用get/set了;不過(guò)configurable與enumerable這兩個(gè)屬性可以與上面兩種屬性任意搭配。
我們?cè)谇懊嬉呀?jīng)說(shuō)了各個(gè)屬性是有默認(rèn)值的,所以在用Object.defineProperty()時(shí)某個(gè)屬性沒(méi)定義不是代表沒(méi)用這條屬性,而是會(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?=?'聽(tīng)風(fēng)是風(fēng)';
//等同于
Object.defineProperty(o,?'name',?{
????value:?'聽(tīng)風(fēng)是風(fēng)',
????writable:?true,
????enumerable:?true,
????configurable:?true
});
//等同于
let?name?=?'聽(tīng)風(fēng)是風(fēng)';
Object.defineProperty(o,?'name',?{
????get()?{
????????return?name;
????},
????set(val)?{
????????name?=?val;
????},
????enumerable:?true,
????configurable:?true
});
關(guān)于屬性分類與默認(rèn)值,如下表:
| configurable | enumerable | value | writable | get | set | |
|---|---|---|---|---|---|---|
| 數(shù)據(jù)描述符 | 可以 | 可以 | 可以 | 可以 | 不可以 | 不可以 |
| 存取描述符 | 可以 | 可以 | 不可以 | 不可以 | 可以 | 可以 |
| 默認(rèn)值 | false | false | false | false | undefined | undefined |
現(xiàn)學(xué)現(xiàn)用,趁熱打鐵
那么到這里,我們?cè)敿?xì)介紹了Object.defineProperty相關(guān)屬性與用法,趁熱打鐵,我們活用它來(lái)解決一些問(wèn)題。原本我想通過(guò)模擬vue數(shù)據(jù)雙向綁定,模擬const以及解決文章開(kāi)頭面試題,但礙于文章篇幅確實(shí)過(guò)長(zhǎng)了,const模擬大家感興趣可自行百度,vue數(shù)據(jù)雙向綁定我會(huì)另起一篇文章,所以這里就來(lái)解決文章開(kāi)頭的題目好了。
我們提取題目細(xì)節(jié),年齡只接受正整數(shù)(在set中判斷),畢竟沒(méi)人是負(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)雅寫(xiě)法歡迎閱讀博主這篇文章?提升代碼幸福度,五個(gè)技巧減少js開(kāi)發(fā)中的if else語(yǔ)句
為什么我不用ES6的class類來(lái)實(shí)現(xiàn)上面的操作了,因?yàn)楣静辉试S使用ES6,去年學(xué)的關(guān)于類好多都忘記了...整理這篇文章也花了好長(zhǎng)時(shí)間,腦袋有點(diǎn)沉,這個(gè)改寫(xiě)就留給各位強(qiáng)大的網(wǎng)友吧。
那么到這里,關(guān)于Object.defineProperty的介紹就結(jié)束了。
補(bǔ)充
關(guān)于上面這道題,考察的雖然是Object.definedProperty的getter與setter,不過(guò)出題人的本意不是希望這么用的,任何對(duì)象在定義時(shí)候可以添加get,set方法,比如:
let?p?=?{
????age_:?27,
????name:?'echo',
????get?age()?{
????????return?this.age_;
????},
????get?name()?{
????????return?'聽(tīng)風(fēng)是風(fēng)'
????}
};
p.name;?//?聽(tīng)風(fēng)是風(fēng)
p.age;?//?27
那么知道了這一點(diǎn),我們來(lái)按照出題人的本意來(lái)分別實(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í)串起來(lái)了,賊開(kāi)心!
??愛(ài)心三連擊 1.看到這里了就點(diǎn)個(gè)在看支持下吧,你的「點(diǎn)贊,在看」是我創(chuàng)作的動(dòng)力。
2.關(guān)注公眾號(hào)
程序員成長(zhǎng)指北,回復(fù)「1」加入高級(jí)前端交流群!「在這里有好多 前端?開(kāi)發(fā)者,會(huì)討論?前端 Node 知識(shí),互相學(xué)習(xí)」!3.也可添加微信【ikoala520】,一起成長(zhǎng)。
“在看轉(zhuǎn)發(fā)”是最大的支持
