像 me.name = 'forceddd'
的賦值語句在JavaScript中是隨處可見的。但是,我們真的瞭解這句程式碼做了什麼事情嗎?是建立了一個新屬性嗎?是修改了原有屬性的值嗎?這次操作是成功還是失敗了呢?這只是一行簡單的賦值語句,但如果我們認真思考的話,就會發現這種細節其實也沒有那麼簡單。
萬物皆可分類討論。首先分為兩類情況:物件 me
上已有 name
屬性和沒有 name
屬性。
當然,我們都知道,JavaScript中存在著原型鏈機制,所以當物件 me
中不存在 name
屬性時,也可以分為兩種情況: me
的原型鏈上存在 name
屬性與不存在 name
屬性。也就是可以分為三類來討論。
me中已存在name屬性
const me = { name: 'me', }; me.name = 'forceddd'; console.log(me.name); //forceddd
在這種情況下顯然我們的目的是重新設定
name
屬性的值,結果似乎也是顯而易見的,me.name
被修改成了'forceddd'
。但是不要忘記了,一個物件的屬性也是擁有它自己的屬性的,其中就包括是否是隻讀的(
writable
),(通常用於描述一個物件的某個屬性的各種特性的物件被稱為屬性描述符或者是屬性描述物件),所以當name屬性的可寫性(writable
)為false
時,結果會是什麼樣的呢?不僅如此,name
屬性定義了getter
或者setter
函式,成為一個訪問描述符的時候,結果又會是怎麼樣的呢?name
屬性的writable
為false
時const me = { name: 'me', }; Object.defineProperty(me, 'name', { writable: false, value: me.name, }); me.name = 'forceddd'; // name的屬性值仍是 'me' console.log(me.name); //me
因為
name
屬性是隻讀的的,所以賦值操作me.name = 'forceddd'
失敗了,這恆河狸。但是要注意的是,這種操作失敗是靜默的,如果我們在操作之後沒有校驗name
值的話,是很難發現的。使用嚴格模式可以把這種靜默失敗變成顯式的TypeError。'use strict';//使用嚴格模式 const me = { name: 'me', }; Object.defineProperty(me, 'name', { writable: false, value: me.name, }); me.name = 'forceddd'; //TypeError: Cannot assign to read only property 'name' of object '#<Object>'
當
name
屬性是訪問描述符(定義了getter
或者setter
)時const me = { _name: 'me', get name() { return this._name; }, set name(v) { console.log('呼叫setter,設定name'); this._name = v; }, }; me.name = 'forceddd';//呼叫setter,設定name console.log(me.name);//forceddd
此時,
name
屬性存在setter
函式,在進行賦值操作時,就會呼叫setter
函式。一般來說,我們在定義訪問描述符時,getter
和setter
都是成對出現的,但是隻定義其中一個也是沒有任何問題的,只是這樣的話可能會出現一些我們不期望的情況,比如,當name
只定義了一個getter
函式時,不存在setter
函式,賦值操作便沒有意義了。非嚴格模式下,因為沒有
setter
函式,所以賦值靜默失敗了。const me = { _name: 'me', get name() { return this._name; }, }; me.name = 'forceddd'; console.log(me.name); //me
嚴格模式下,對沒有
setter
函式的訪問描述符進行賦值操作,會出現一個TypeError,也是非常河狸的。'use strict'; const me = { _name: 'me', get name() { return this._name; }, }; me.name = 'forceddd'; //TypeError: Cannot set property name of #<Object> which has only a getter
總結一下,當
me
中存在name
屬性時,進行賦值操作時,存在三種情況:- 當屬性是一個訪問描述符時,如果存在
setter
,則呼叫setter
;如果不存在setter
,在非嚴格模式下會靜默失敗,在嚴格模式下會產生一個TypeError。 - 當屬性的屬性描述符中可寫性(
writable
)為false
時,在非嚴格模式下會靜默失敗,在嚴格模式下會產生一個TypeError - 當屬性不屬於上面兩種情況時,將值設為該屬性的值,賦值成功。
me及其原型鏈上均不存在name屬性
此時,會在物件
me
上新建一個屬性name
,值為'forceddd'
。這是也是我們經常使用的一種方式。const human = {}; const me = {}; Object.setPrototypeOf(me, human); me.name = 'forceddd'; console.log({ me, human }); //{ me: { name: 'forceddd' }, human: {} }
我們使用這種方式建立的屬性當然是可以通過賦值的方式修改的,這也就是代表著此時這個屬性的屬性描述符中的
writable
為true
。事實上,此時屬性的可配置性、可列舉性也都為true
,這和通過defineProperty
新增屬性的預設值是不同的,defineProperty
方法預設新增的屬性描述符中writable
、configurable
、enumerable
預設值都為false
。這也是值得注意的一點。console.log(Object.getOwnPropertyDescriptor(me, 'name')); // { // value: 'forceddd', // writable: true, // enumerable: true, // configurable: true // }
通過
defineProperty
方法新增屬性Object.defineProperty(me, 'prop', { value: 'forceddd' }); console.log(Object.getOwnPropertyDescriptor(me, 'prop')); // { // value: 'forceddd', // writable: false, // enumerable: false, // configurable: false // }
me中不存在name屬性,而其原型鏈上存在name屬性
此時,
me
的原型物件human
上存在name
屬性,很多時候,我們使用me.name = 'forceddd'
之類的賦值語句,只是為了修改物件me
,而不想牽涉到其他物件。但是在JS中,訪問物件屬性時,如果該屬性在物件上不存在,便會去原型鏈上查詢,所以物件的原型鏈是很難繞開的。const human = {}; const me = {}; Object.setPrototypeOf(me, human);
如前文所述,
name
屬性在human
物件上也是有三種情況:name
屬性的屬性描述符中writable
為false
//設定human物件的name屬性 Object.defineProperty(human, 'name', { writable: false, value: 'human', }); me.name = 'forceddd'; console.log(me);//{}
此時,當我們進行了賦值操作後,檢查
me
物件,會發現它並沒有name
屬性。WTF?很難理解是吧,因為human
中的name
屬性是隻讀的,所以將human
物件為原型的物件就不能通過=
賦值操作新增name
屬性了。其實這是為了模仿類的繼承行為,如果父類中的
name
屬性是隻讀的,那麼繼承它的子類中的name
屬性自然應該也是隻讀的。又因為在JS中時通過原型鏈來模擬的類的繼承行為,所以就導致了這一看起來很奇怪的現象。同樣地,如果是在嚴格模式下,不會是靜默失敗了,而是會產生一個TypeError。
name
屬性是一個訪問描述符Object.defineProperty(human, 'name', { set() { console.log('呼叫了human的setter'); } }); me.name = 'forceddd';//'呼叫了human的setter' console.log(me);//{}
此時,同樣不會在
me
物件上建立name
屬性,而是會呼叫human
上name
屬性的setter
函式。類似地,當只存在getter
,不存在setter
時,嚴格模式下會產生TypeError,非嚴格模式下會靜默失敗。name
屬性不是之前兩種情況這種情況最簡單,也是最符合我們預想的,會在
me
物件上建立一個name
屬性。
總結一下:當
me
中不存在name
屬性,而其原型鏈上存在name
屬性時,有三種情況:- 如果該屬性在原型鏈上是一個訪問描述符並且存在
setter
,則會呼叫這個setter
;如果只存在getter
,則非嚴格模式下會靜默失敗,嚴格模式下出現TypeError。 - 如果該屬性在原型鏈是是一個
writable
為false
的只讀屬性,在非嚴格模式下會靜默失敗,嚴格模式下出現TypeError。 - 如果該屬性在原型鏈上不是前兩種情況,只是一個
writable
為true
的普通屬性,則會在me
物件上建立該屬性。
如果你一定、必須要修改或者設定 me
的 name
屬性的話,可以使用 defineProperty
方法來繞過 =
賦值的這些限制。
Object.defineProperty(human, 'name', {
set() {
console.log('呼叫了human的setter');
// me.name = 'forceddd';
},
});
Object.defineProperty(me, 'name', {
value: 'forceddd',
enumerable: true,
writable: true,
configurable: true,
});
console.log(me); //{ name: 'forceddd' }
文字上的描述總是沒有那麼直觀,最後總結為一張圖,便於理解。