你真的瞭解Object.defineProperty嗎

1. 前言

想到Object.defineProperty,首先不得不提到對象,對象是JavaScript的基礎,有一種常見的說法「JavaScript中萬物皆是對象」。javascript

這種說法其實並不那麼準確,根據 JavaScript 對語言類型的分類,就能夠得出JavaScript並非萬物皆對象,此次就不對這個問題進行展開,感興趣能夠點擊JavaScript 萬物皆對象🤔。可是足以證實對象對於JavaScript這門語言的重要性。java

Object.defineProperty這個方法最經常使用的場景應該是在面試的時候,每當面試官問起Vue雙向綁定的原理,不少朋友可能破口而出就是這個方法,好了不開玩笑了,進入正題。面試

Object.defineProperty() 方法會直接在一個對象上定義一個新屬性,或者修改一個對象的現有屬性, 並返回這個對象。數組

該方法容許精確添加或修改對象的屬性。經過賦值操做添加的普通屬性是可枚舉的,可以在屬性枚舉期間呈現出來(for...inObject.keys方法),這些屬性的值能夠被改變,也能夠被刪除。這個方法容許修改默認的額外選項(或配置)。默認狀況下,使用 Object.defineProperty() 添加的屬性值是不可修改的函數

以上是MDN對於這個方法的描述post

2. 屬性描述符

在ES5以前,JavaScript語言自己並無提供能夠直接檢測屬性特性的方法,可是從ES5開始,全部的屬性都具有了屬性描述符,有兩種主要形式:ui

  • 數據描述符
    • 數據描述符是一個具備值的屬性,該值多是可寫的,也可能不是可寫的
  • 訪問描述符
    • 訪問描述符是由getter-setter函數對描述的屬性。

描述符必須是這兩種形式之一,不能同時是二者,爲何後面會具體討論。this

2.1 Writable

writable決定是否能夠修改屬性的值。spa

let myObject = {}

Object.defineProperty(myObject, 'a', {
    value: 2,
    writable: false, // 不可寫
    configurable: true,
    enumerable: true
})

myObject.a = 3

console.log(myObject.a) // 2
複製代碼

若是在嚴格模式下,會拋出TypeError錯誤表示咱們沒法修改一個不可寫的屬性。雙向綁定

2.2 Configurable

Configurable決定對象屬性是否可配置,只要屬性是可配置的,就可使用defineProperty()方法來修改屬性描述符:

let myObject = {
  a: 2
}

myObject.a = 3
console.log(myObject.a) // 3

Object.defineProperty(myObject, 'a', {
  value: 4,
  writable: true,
  configurable: false, // 不可配置
  enumerable: true
})

console.log(myObject.a) // 4
myObject.a = 5
console.log(myObject.a) // 5

Object.defineProperty(myObject, 'a', {
  value: 4,
  writable: true,
  configurable: true, // 修改成可配置
  enumerable: true
}) // TypeError
複製代碼

不論是不是處於嚴格模式,嘗試修改一個不可配置的描述符都會拋出錯誤,如你所見,把configurable修改爲false是一個單向操做,沒法撤銷!

有一個小小的例外,即便configurable: false,咱們仍是能夠把writable的狀態由true改成false,可是沒法由false改成true

除了沒法修改,configurable: false還會禁止刪除這個屬性:

let myObject = {
  a: 2
}

console.log(myObject.a) // 2
delete myObject.a
console.log(myObject.a) // undefined

Object.defineProperty(myObject, 'a', {
  value: 2,
  writable: true,
  configurable: false, // 不可配置
  enumerable: true
})

console.log(myObject.a) // 2
delete myObject.a
console.log(myObject.a) // 2
複製代碼

最後一個delete語句失敗了,由於屬性不可配置。

2.3 Enumerable

從名字就能夠看出,這個描述符控制的是屬性是否出如今對象的屬性枚舉中,好比for...in循環。

let myObject = {
  a: 2
}

Object.defineProperty(myObject, 'a', {
  value: 2,
  enumerable: true // 可枚舉
})

Object.defineProperty(myObject, 'b', {
  value: 2,
  enumerable: false // 不可枚舉
})

console.log(myObject.b) // 2
console.log('b' in myObject) // true
console.log(myObject.hasOwnProperty('b')) // true

for (var k in myObject) {
  console.log(k, myObject[k])
} // 'a' 2
複製代碼

能夠看到,myObject.b確實存在而且有訪問值,可是卻不會for...in循環中,儘管它確實存在於myObject對象中。

inhasOwnProperty的區別在因而否查找[[Prototype]]鏈,in會沿着原型鏈往上查找

再看一個實例:

let myObject = {
  a: 2
}

Object.defineProperty(myObject, 'a', {
  value: 2,
  enumerable: true // 可枚舉
})

Object.defineProperty(myObject, 'b', {
  value: 2,
  enumerable: false // 不可枚舉
})

console.log(myObject.propertyIsEnumerable('a')) // true
console.log(myObject.propertyIsEnumerable('b')) // false

console.log(Object.keys(myObject)) // ['a']
console.log(Object.getOwnPropertyNames(myObject)) // ['a', 'b']
複製代碼

propertyIsEnumerable()會檢查給定的屬性名是否直接存在於對象中,而且知足enumerable: true

Object.keys()會返回一個數組,包含全部的可枚舉屬性。Object.getOwnPropertyNames()也會返回一個數組,包含全部屬性,不管它們是否可枚舉,這兩個方法都只會查找對象直接包含的屬性。

2.4 Getter和Setter

gettersetter能夠改寫默認操做,可是隻能應用在單個屬性上,沒法應用在整個對象,gettersetter都是隱藏函數,getter會在獲取屬性值時調用,setter會在設置屬性值時調用。好比Vue就會給全部的屬性添加上gettersetter函數。

當你給一個屬性定義gettersetter或者二者都有時,這個屬性會被定義爲「訪問描述符」(和「數據描述符」相對)。

對於訪問描述符來講,JavaScript會忽略它們的valuewritable特性,取而代之的是關心setget(還有configurableenumerable)特性。這就是爲何數據描述符和訪問描述符只會存在一個的緣由。

let myObject = {
  get a() {
    return 2
  }
}

Object.defineProperty(myObject, 'b', {
  get: function () {
    return this.a * 2
  }
})

console.log(myObject.a) // 2
console.log(myObject.b) // 4
複製代碼

我經過兩種方式建立了兩個不包含值的屬性,可是在訪問這兩個屬性的時候,它們都會自動調用一個隱藏的get函數,函數的返回值會被看成屬性訪問的返回值。

let myObject = {
  get a() {
    return 2
  }
}

myObject.a = 3

console.log(myObject.a) // 2
複製代碼

因爲定義了a屬性的getter,因此對a的值進行設置時set操做會忽略賦值操做。並且即使有合法的setter,因爲咱們自定義的getter只會返回2,因此set操做時沒有意義的。

一般,gettersetter時成對出現的(只定義一個的話一般會產生意料以外的行爲):

let myObject = {
  get a() {
    return this._a_
  }, // 給a定義一個getter
  set a(val) {
    this._a_ = val * 2
  } // 給a定義一個setter
}

myObject.a = 2

console.log(myObject.a) // 4
複製代碼

當對目標對象的屬性設置了writable:false,至關於你定義了一個空操做setter,你的全部set操做都會被忽略。

3. 補充(對象的不變性)

有時候你會但願屬性或者對象是不可改變的,在ES5中有不少方法能夠實現。很重要的一點,全部的方法建立的都是淺不可變性,也就是說,它們只會影響目標對象和它的直接屬性,若是目標對象引用了其餘對象,其餘對象的內容仍然是可變的。

3.1 對象常量

結合writable:falseconfigurable:false就能夠建立一個真正的常量屬性,不可修改、從新定義或者刪除。

3.2 禁止擴展

若是你但願獲得一個對象,它禁止添加新屬性而且保留已有屬性,可使用Object.preventExtensions():

let myObject = {
  a: 2
}

Object.preventExtensions(myObject)

myObject.b = 3
console.log(myObject.b) // undefined
複製代碼

3.3 密封

Object.seal()會建立一個「密封」的對象,這個方法實際上會在一個現有對象上調用Object.preventExtensions()並把全部現有屬性標記爲configurable:false

密封以後不只不能添加新屬性,也不能從新配置或者刪除任何現有屬性,可是能夠修改屬性的值。

3.4 凍結

Object.freeze()會建立一個凍結對象,這個方法實際上會在一個現有對象上調用Object.seal()並把全部「數據訪問」屬性標記爲writable:false,這樣就不可修改它們的值。

這個方法是能夠應用在對象上的級別最高的不可變性,它會禁止對於對象自己及其任意直接屬性的修改。

以上的幾個方法都要慎用,頗有可能帶來意想不到的行爲。

4. 參考

  • 《你不知道的JavaScript(上卷)》第二部分第三章:對象

5. 最後

文中若是有問題和遺漏,歡迎在評論區指出,若是本文能給幫助到你,請給個點贊👍和關注

本文到此結束,886🚀🚀

相關文章
相關標籤/搜索