Object.defineProperty()
Object.defineProperty():
這個方法會直接在一個對象上定義一個新屬性,或者修改一個對象現有的屬性,並返回這個對象。
對象定義屬性和賦值
在對象中,我們有很多種方式給其定義屬性和賦值。最常見的是obj.prop = value
和obj[‘prop‘] = value
。比如:
let Person = {}
Person.name = ‘大漠‘
Person[‘age‘] = 35
console.log(Person)
除了上述的方式之外,還可以使用Object.defineProperty()
方法來定義和修改對象屬性。下面我們就來好好的探討一下這個方法。
Object.defineProperty()語法
Object.defineProperty()
的作用就是直接在一個對象上定義一個新屬性,或者修改一個已經存在的屬性,並返回這個對象,我們先來看一下怎麽使用這個方法:
Object.defineProperty(obj, prop, descriptor)
這個方法有三個參數:
obj
:需要被定義(或修改)屬性的那個對象prop
:需要被定義(或修改)的屬性名descriptor
:定義(或修改)的屬性prop
的描述
其返回值是被傳遞給函數的對象。
該方法允許精確添加或修改對象的屬性。一般情況下,為對象添加屬性是通過賦值來創建並顯示在屬性枚舉中(for...in
Object.keys
方法),但這種方式添加的屬性值可以被改變,也可以被刪除。而使用Object.defineProperty()
則允許改變這些額外細節的默認設置。例如,默認情況下,使用Object.defineProperty()
增加的屬性值是不可改變的。
屬性特性和內部屬性
JavaScript中有三種類型的屬性:
- 命名數據屬性:擁有一個確定的值的屬性。這也是最常見的屬性
- 命名訪問器屬性:通過
getter
和setter
進行讀取和賦值的屬性 - 內部屬性:由JavaScript引擎內部使用的屬性,不能通過JavaScript代碼直接訪問到,不過可以通過一些方法間接的讀取和設置。比如,每個對象都有一個內部屬性
[[Prototype]]
Object.getPrototypeOf()
方法間接的讀取到它的值。雖然內部屬性通常用一個雙呂括號包圍的名稱來表示,但實際上這並不是它們的名字,它們是一種抽象操作,是不可見的,根本沒有上面兩種屬性有的那種字符串類型的屬性名
屬性特性
對象中每個屬性都有四個特性。兩種類型的屬性一共有六個屬性特性:
- 命名數據屬性特有的特性:屬性的值的
[[Value]]
特性和控制屬性的值是否可以修改的[[Writable]]
特性 - 命名訪問器屬性特有的特性:存儲著
getter
方法的[[Get]]
和存儲著setter
方法的[[Set]]
- 兩種屬性都有的特性:如果一個屬性是不可枚舉的,則一些操作下,這個屬性是不可見的,比如
for...in
和Object.keys
,那麽可以通過[[Enumerable]]
特性來設置;如果一個屬性是不可配置的,則該屬性的所有特性([[Value]]
)都不可改變,那麽可以通過[[Configurable]]
特性來設置
內部屬性
除了上面所說的之外,下面幾個是所有對象都有的內部屬性:
[[Prototype]]
:對象的原型[[Extensible]]
:對象是否可擴展,也就是是否可以添加新的屬性[[DefineOwnProperty]]
:定義一個屬性的內部方法[[Put]]
:為一個屬性賦值的內部方法
屬性描述符
對象裏目前存在的屬性描述符有兩種主要形式:數據描述符和存取描述符。
- 數據描述符:是一個擁有可寫或不可寫值的屬性
- 存取描述符:是由一對
getter
、setter
函數功能來描述的屬性
描述符必須是兩種形式之一;不能同時是兩者。另外,屬性描述符可以將一個屬性的所有特性編碼成一個對象並返回。例如:
Object.defineProperty(obj, ‘key‘, {
enumerable: false,
configurable: false,
writable: false,
value: ‘static‘
})
屬性描述符除了在Object.defineProperty()
中使用之外,還常常使用在Object.getOwnPropertyDescriptor()
和Object.create()
中。如果對象中的某個屬性省略了屬性描述符,則該屬性會取一個默認值:
屬性名 | 默認值 |
---|---|
value |
undefined |
get |
undefined |
set |
undefined |
writable |
false |
enumerable |
false |
configurable |
false |
先對屬性描述符做一個簡單的歸納,因為後面接下來的篇幅都將圍繞著Object.defineProperty()
的屬性描述符descriptor
來展開的。
數據描述符和存取描述均具有以下可選鍵值:
configurable
:這個特性決定了對象的屬性是否可以被刪除,以及除writable
特性外的其它特性是否可以被修改;並且writable
特性值只可以是false
。默認為false
。同樣通過示例來幫助我們理解這個描述符的特性:
let Person = {}
Object.defineProperty(Person, ‘name‘, {
value: ‘大漠‘,
configurable: false
})
console.log(Person.name) // => 大漠
delete Person.name
console.log(Person.name) // => 大漠
可以看出,雖然執行了delete Person.name
,但name
的屬性值並沒有刪除。這主要是因為,configurable
的值為false
,不允許刪除。如果我們把其值設置為true
,但結果就不一樣了:
let Person = {}
Object.defineProperty(Person, ‘name‘, {
value: ‘大漠‘,
configurable: false
})
console.log(Person.name) // => 大漠
delete Person.name
console.log(Person.name) // => undefined
在上面的示例基礎上,咱們再添加writable
的描述符,首先來看writable
為true
的情況:
let Person = {}
Object.defineProperty(Person, ‘name‘, {
value: ‘大漠‘,
configurable: false,
writable: true
})
console.log(Person.name) // => 大漠
delete Person.name
console.log(Person.name) // => 大漠
另外來看writable: true
的情況:
let Person = {}
Object.defineProperty(Person, ‘name‘, {
value: ‘大漠‘,
configurable: false,
writable: false
})
console.log(Person.name) // => 大漠
delete Person.name
console.log(Person.name) // => 大漠
再來看看另外兩種情況:
let Person = {}
Object.defineProperty(Person, ‘name‘, {
value: ‘大漠‘,
configurable: true,
writable: true
})
console.log(Person.name) // => 大漠
delete Person.name
console.log(Person.name) // => undefined
// 或
let Person = {}
Object.defineProperty(Person, ‘name‘, {
value: ‘大漠‘,
configurable: true,
writable: false
})
console.log(Person.name) // => 大漠
delete Person.name
console.log(Person.name) // => undefined
enumerable
:僅當該屬性的enumerable
為true
時,該屬性才能夠出現在對象的枚舉屬性中。默認為false
。也就是說,當enumerable
的值為true
時,才可以使用for(prop in obj)
和Object.keys()
進行枚舉。
let Person = {}
Object.defineProperty(Person, ‘name‘, {
value: ‘大漠‘,
enumerable: true // 可以枚舉
})
Object.defineProperty(Person, ‘age‘, {
value: 35,
enumerable: false // 不可枚舉
})
Object.defineProperty(Person, ‘title‘, {
value: ‘切圖仔‘ // enumerable取默認值,為false
})
Person.from = ‘W3cplus‘ // 如果使用直接賦值的方式創建對象的屬性,則這個屬性的enumerable為true
for (let i in Person) {
console.log(i) // => name, from
}
Object.keys(Person)
Person.propertyIsEnumerable(‘name‘) // => true
Person.propertyIsEnumerable(‘age‘) // => false
Person.propertyIsEnumerable(‘title‘) // => false
Person.propertyIsEnumerable(‘from‘) // => true
數據描述符同時具有以下可選鍵值:
value
:該屬性對應的值。可以是任何有效的JavaScript值(數值、對象、函數等)。默認為undefined
。來看一個小示例:
let Person = {}
Object.defineProperty(Person, ‘name‘, {
value: ‘大漠‘
})
console.log(Person)
從上面的結果中我們可以看到,我們給Person
定義了一個新的屬性name
,然後我們打印這個對象就是我們預期的那樣,其中對象Person.name
的值為‘大漠‘
。在上面的基礎上,我們來通過普通的obj.name=value
這樣的方式重新給對象Person
中的name
屬性賦值:
let Person = {}
Object.defineProperty(Person, ‘name‘, {
value: ‘大漠‘
})
console.log(Person)
Person.name = ‘w3cplus_大漠‘
console.log(Person)
正如大家所看到的,盡管我們使用Person.name=‘w3cplus_大漠‘
的方式,給對象Person
中的name
屬性重新賦值‘w3cplus_大漠‘
,卻發現這個屬性name
並沒有得到改變,還是以第一次我們賦給它的值。主要原因是,這個屬性的writable
默認值為false
,需要將writable
修飾符重置為true
。name
屬性才可以被修改。
writable
:僅當該屬性的writable
為true
時,該屬性的屬性值才能被改變。默認為false
。回到上面的示例中來,如果我們想把Person
對象中的name
值修改成我們所期望的屬性值,那麽就得在Object.defineProperty()
定義name
屬性時,就指定該屬性的writable
的描述符值為true
。
let Person = {}
Object.defineProperty(Person, ‘name‘, {
value: ‘大漠‘,
writable: true
})
console.log(Person)
Person.name = ‘w3cplus_大漠‘
console.log(Person)
正如你所看到的結果,我們可以重新設置name
的屬性值。
存取描述符同時具有以下可選鍵值:
get
:一個給屬性提供getter
的方法,如果沒有getter
則為undefined
。該方法返回值被用作屬性值。默認為undefined
。
下面這個示例說明了如何實現自我存檔的對象。當temperature
屬性設置時,archive
數組會得到一個log
。
function Archiver () {
let temperature = null
let archive = []
Object.defineProperty(this, ‘temperature‘, {
get: function () {
console.log(‘Get!‘)
return temperature
}
})
this.getArchive = function () {
return archive
}
}
let arc = new Archiver()
arc.temperature
arc.temperature = 11
arc.temperature = 13
arc.getArchive()
輸出結果如下:
set
:一個給屬性提供setter
的方法,如果沒有setter
則為undefined
。該方法將接受唯一參數,並將該參數的新值分配給該屬性。默認值為undefined
。
function Archiver () {
let temperature = null
let archive = []
Object.defineProperty(this, ‘temperature‘, {
get: function () {
console.log(‘Get!‘)
return temperature
},
set: function (value) {
temperature = value
archive.push({
val: temperature
})
}
})
this.getArchive = function () {
return archive
}
}
let arc = new Archiver()
arc.temperature
arc.temperature = 11
arc.temperature = 13
arc.getArchive()
記住,這些選項不一定是自身屬性,如果是繼承來的也要考慮。為了確認保留這些默認值,你可能要在這之前凍結Object.prototype
,明確指定所有的選項,或者將__proto__
屬性指向null
。
屬性定義和屬性賦值
@Dr. Axel Rauschmayer寫了一篇文章,詳細的闡述了JavaScript中的屬性定義和賦值的區別。摘取文章中的內容,簡單的看看JavaScript中的屬性定義和屬性賦值。
屬性定義
定義屬性是通過內部方法來進行操作的:
[[DefineOwnProperty]](P, Desc, Throw)
P
是要定義的屬性名稱,參數Throw
決定了在定義操作被拒絕的時候是否要拋出異常。如果Throw
為true
,則拋出異常;否則,操作只會靜默失敗。當調用[[DefineOwnProperty]]
時,具體會執行下面的操作步驟。
- 如果
this
沒有名為P
的自身屬性的話:如果this
是可擴展的話,則創建P
這個自身屬性,否則拒絕 - 如果
this
已經有了名為P
的自身屬性:則按照下面的步驟重新配置這個屬性 - 如果這個已有的屬性是不可配置的,則進行下面的操作會被拒絕:將一個數據屬性轉換成訪問器屬性,反之變然;改變
[[Configurable]]
或[[Enumerable]]
;改變[[Writable]]
;在[[Writable]]
為false
時改變[[Value]]
和改變[[Get]]
或[[Set]]
- 否則,這個已有的屬性可以被重新配置
如果Desc
就是P
屬性當前的屬性描述符,則該定義操作永遠不會被拒絕。
定義屬性的函數有兩個:Object.defineProperty
和Object.defineProperties
。例如:
Object.defineProperty(obj, propName, desc)
在引擎內部,會轉換成這樣的方法調用:
obj.[[DefineOwnProperty]](propName, desc, true)
屬性賦值
為屬性賦值是通過內部方法進行操作的:
[[Put]](P, Value, Throw)
參數P
以及Throw
和[[DefineOwnProperty]]
方法中的參數表現的一樣。在調用[[Put]]
方法的時候,會執行下面這樣的操作步驟:
- 如果在原型鏈上存在一個名為
P
的只讀屬性(只讀的數據屬性或者沒有setter
的訪問器屬性),則拒絕 - 如果在原型鏈上存在一個名為
P
的且擁有setter
的訪問器屬性,則調用這個setter
- 如果沒有名為
P
的自身屬性,則如果這個對象是可擴展的,就使用下面的操作創建一個新屬性,否則,如果這個對象是不可擴展的,則拒絕 - 否則,如果已經存在一個可寫的名為
P
的自身屬性,則調用this.[[DefineOwnProperty]](P, {value: Value}, Throw)
。該操作只會更改P
屬性的值,其他的特性(比如可枚舉性)都不會改變
賦值運算符=
就是在調用[[Put]]
。比如:
Obj.prop = value
在引擎內部,會轉換成這樣的方法調用:
Obj.[[Put]](‘prop‘, value, isStrictModeOn)
isStrictModeOn
對應著參數Throw
。也就是說,賦值運算符只有在嚴格模式下才有可能拋出異常。[[Put]]
沒有返回值,但賦值運算符有。
作用及影響
屬性的定義操作和賦值操作各自有自己的作用和影響。
賦值可能會調用原型上的setter
,定義會創建一個自身屬性。比如,給一個空對象obj
,他的原型proto
有一個名為foo
的訪問器屬性。
let proto = {
get foo() {
console.log(‘Getter!‘)
return ‘a‘
},
set foo(x) {
console.log(`Setter: ${x}`)
}
}
let obj = Object.create(proto)
console.log(obj)
那麽,在obj
身上定義一個foo
屬性和為obj
的foo
屬性賦值有什麽區別呢?
如果是定義操作的話,則會在obj
身上添加一個自身屬性foo
:
Object.defineProperty(obj, ‘foo‘, {
value: ‘b‘
})
console.log(obj.foo)
console.log(proto.foo)
但如果為foo
屬性賦值的話,則意味著你想改變某個已經存在的屬性的值。所以這次賦值操作會轉交給原型proto
的foo
屬性的setter
訪問器來處理。下面代碼的執行結果就能證明這一結論:
你還可以定義一個只讀的訪問器屬性,辦法是:只定義一個getter
,省略setter
。下面的例子中,proto
的bar
屬性就是這樣的只讀屬性,obj
繼承了這個屬性。
‘use strict‘;
let proto = {
get bar() {
console.log(‘Getter!‘)
return ‘a‘
}
}
let obj = Object.create(proto)
console.log(obj)
在開啟嚴格模式的話,下面的賦值操作會拋出異常。非嚴格模式的話,賦值操作只會靜默失敗(不起任何作用,也不報錯)
obj.bar = ‘b‘
console.log(obj.bar)
我們還可以定義一個自身屬性bar
,遮蔽從原型上繼承的bar
屬性:
Object.defineProperty(obj, ‘bar‘, {
value: ‘b‘
})
console.log(obj.bar)
console.log(proto.bar)
原型鏈中的同名只讀屬性可能會阻止賦值操作,但不會阻止定義操作。如果原型鏈中存在一個同名的只讀屬性,則無法通過賦值的方式在原對象上添加這個自身屬性,必須使用定義操作才可以。這項限制是在ECMAScript 5.1中引入的:
‘use strict‘
let proto = Object.defineProperties(
{},
{
foo: { // foo屬性的特性
value: ‘a‘, // foo屬性的值
writable: false, // 只讀
configurable: true // 可配置
}
}
)
let obj = Object.create(proto)
console.log(obj)
賦值操作會導致異常:
obj.foo = ‘b‘
console.log(obj.foo)
通過定義的方式,我們可以成功創建一個新的自身屬性:
Object.defineProperty(obj, ‘foo‘, {
value: ‘b‘
})
console.log(obj.foo) // => b
console.log(proto.foo) // => a
賦值運算符不會改變原型鏈上的屬性。執行下面的代碼,則obj
會從proto
上繼承到foo
屬性。
let proto = {
foo: ‘a‘
}
let obj = Object.create(proto)
console.log(obj)
不能通過為obj.foo
賦值來改變proto.foo
的值。這種操作只會在obj
上新建一個自身屬性。
obj.foo = ‘b‘
console.log(obj.foo) // => b
console.log(proto.foo) // => a
只有通過定義操作,才能創建一個擁有指定特性的屬性。如果通過賦值操作創建一個自身屬性,則該屬性始終擁有默認的特性。如果你想指定某個特性的值,必須通過定義操作。
對象字面量中的屬性是通過定義操作添加的。下面的代碼將變量obj
指向一個對象字面量:
let obj = {
name: ‘大漠‘
}
這樣的語句在引擎內部,可能會被轉換成下面兩種操作方式中的一種。首先可能是賦值操作:
let obj = new Object()
obj.name = ‘大漠‘
其次,可能是個定義操作:
let obj = new Object()
Object.defineProperties(obj, {
name: {
value: ‘大漠‘,
enumerable: true,
configurable: true,
writable: true
}
})
到底是哪種呢?正確答案是第二種。因為第二種操作方式能夠更好的表達出對象字面量的語義:創建新的屬性Object.create()
接受一個屬性描述符作為第二個可選參數,也是這個原因。
可以通過定義操作新建一個只讀的方法屬性:
‘use strict‘
function Stack() {
}
Object.defineProperties(Stack.prototype, {
push: {
writable: false,
configurable: true,
value: function (x) {
console.log(x)
}
}
})
目的是為了防止在實例身上發生意外的賦值操作。
let s = new Stack()
s.push = 5
不過,由於push
是可配置的,所以我們仍可以通過定義操作來為實例添加一個自身的push
方法。
let s = new Stack()
Object.defineProperty(s, ‘push‘, {
value: function () {
return ‘yes‘
}
})
console.log(s.push()) // => yes
我們甚至可以通過定義操作來重新定義原型上的push
方法:Stack.prototype.push
。
添加多個屬性和默認值
考慮特性被賦予的默認特性值非常重要,通常,使用點運算符和Object.defineProperty()
為對象的屬性賦值時,數據描述符中的屬性默認值是不同的,如下例所示。
let obj = {}
obj.name = ‘大漠‘
上面的代碼等同於:
Object.defineProperty(obj, ‘name‘, {
value: ‘大漠‘,
writable: true,
configurable: true,
enumerable: true
})
另一主面:
Object.defineProperty(obj, ‘name‘, {
value: ‘大漠‘
})
等同於:
Object.defineProperty(obj, ‘name‘, {
value: ‘大漠‘,
writable: false,
configurable: false,
enumerable: false
})
總結
這篇文章主要介紹了Object.defineProperty(obj, prop,descriptor)
。我想大家和我一樣,對這個方法應該有了一定的了解。有了這個基礎,咱們回過頭來理解《學習Vue的雙向綁定原理及實現》一文中提到的Vue的雙向數據綁定的原理就變得容易一些了。而且這個屬性也能更好的幫助我們後面理解Vue響應式視圖,或者說Vue中的計算屬性的奧秘。
著作權歸作者所有。
商業轉載請聯系作者獲得授權,非商業轉載請註明出處。
原文: https://www.w3cplus.com/javascript/object-defineproperty.html © w3cplus.com
Object.defineProperty()