iT邦幫忙

2019 iT 邦幫忙鐵人賽

DAY 2
1
Modern Web

我或許沒那麼懂 Web系列 第 2

JS Object.defineProperty(): (1) descriptor

這是在瀏覽 Vue.js 原始碼時看到的方法。是在 ECMAScript 5.1 的規格中被定義的。

以 Vue.js 中 /src/core/index.js 裡部分的程式碼為例

Object.defineProperty(Vue.prototype, '$isServer', {
  get: isServerRendering
})

Object.defineProperty(Vue.prototype, '$ssrContext', {
  get () {
    return this.$vnode && this.$vnode.ssrContext
  }
})

Object.defineProperty(Vue, 'FunctionalRenderContext', {
  value: FunctionalRenderContext
})

剛看到方法名時是感到疑惑的,想說物件屬性(Property)[^1]不是用 object['propertyName'] 就可以定義了嗎?為什麼要特別有這個方法?接著又看到裡面定義了 get() 方法,似乎有點感覺了,繼續研究下去看看吧。

首先看個 MDN Web docs 關於 Object.defineProperty() 的介紹,這邊只引用署名式(signature),關於參數、返回值的定義,請自行參見該文件。

Object.defineProperty(obj, prop, descriptor)

而平時我們用 object['propertyName'] = 去賦值的行為,等價於透過 Object.defineProperty() 特別設定 descriptor的行為,如下:

'use strict'

let obj = {}

// 下面兩則敘述在行為上是等價的。
obj['key'] = 'value'
Object.defineProperty(obj, 'key', {
  enumerable: true,
  configurable: true,
  writable: true,
  value: 'value'
})

為什麼要說特別設定呢,因為 descriptorenumerableconfigurablewritable 這些特性(attributes)[^1]的預設值其實都是 false,如下:

'use strict'

let obj = {}

// 下面兩則敘述在行為上是等價的。
Object.defineProperty(obj, 'key', { value: 'value' })
Object.defineProperty(obj, 'key', {
  enumerable: false,
  configurable: false,
  writable: false,
  value: 'value'
})

descriptor 中的這些特性分別代表什麼意思呢?value 較好懂,即為查詢這個屬性會得到的值。但除此之外還有 enumerableconfigurablewritable三種特性、以及 get()set()兩個方法可以定義,但今天先不討論後兩個方法。

因為平時透過 = 賦值的狀況,與特性為 true 時的行為相同,所以這邊以特性為 false 的狀況去介紹,剛好也等於 Object.defineProperty() 的預設值。

enumerablefalse時,代表不可列舉。所謂的不可列舉就是當我們呼叫 Object.keys() 或透過 for...in 迴圈時,是找不到該屬性的。

'use strict'

let obj = {}

// enumerable: true
obj['key'] = 'value'
console.log(obj)

// enumerable: false
Object.defineProperty(obj, 'key', { enumerable: false })
console.log(obj) 

// enumerable: true
Object.defineProperty(obj, 'key', { enumerable: true })
console.log(obj) 

// Output:
//   { key: 'value' }
//   {}
//   { key: 'value' }

writablefalse 時,代表不可覆寫。所謂的不可覆寫就是該屬性無法重新賦值,不管你在設定 writable 時,該屬性有沒有值,在設定後都無法透過 = 重新賦值,除非再用 Object.defineProperty() 去設定。

'use strict'

let obj = {}

// writable: true
obj['key'] = 'value' 
console.log(obj['key'])

obj['key'] = 'value1'
console.log(obj['key'])

Object.defineProperty(obj, 'key', { value: 'value2' })
console.log(obj['key'])


// writable: false
Object.defineProperty(obj, 'key', { writable: false })

// Available
Object.defineProperty(obj, 'key', { value: 'value3' })
console.log(obj['key'])

// Unavailable
obj['key'] = 'value4'  

// Output:
//   static-value
//   static-value1
//   static-value2
//   static-value3
//   TypeError: Cannot assign to read only property 'key' of object

configurablefalse時,代表不可再設定。所謂的不可再設定就是若要再透過Object.defineProperty() 變動 descriptor 中的 enumerableconfigurableget()set() 的值時,都會跳出 TypeError。而由於後兩者是透過參照的方式,所以就算重新給一個敘述完全一樣的函式,都算是變動了參照,也會回報錯誤。

writablevalue 都是可以再設定的嗎?是的,但是這是在 writabletrue 的情況。若在 configurablewritable 皆為 false 的情況,透過 Object.defineProperty()變動兩者的值或是透過 = 更改 value 的值時,一樣都會出現 TypeError。這部分要特別注意,我自己也是被這件事困惑過。

'use strict'

let obj = {}

// configurable: true
// writable: true
obj['key'] = 'value' 
console.log(obj)

// Available
Object.defineProperty(obj, 'key', { value: 'value1' })
console.log(obj)


// configurable: false
// writable: true
Object.defineProperty(obj, 'key', { 
  configurable: false,
  writable: true ,
  enumerable: true 
})

// Available
obj['key'] = 'value2' 
console.log(obj)

// Available
Object.defineProperty(obj, 'key', { value: 'value3' }) 
console.log(obj)


// configurable: false
// writable: false
Object.defineProperty(obj, 'key', { writable: false })

// Unavailable
obj['key'] = 'value4' 
console.log(obj)

// Unavailable
Object.defineProperty(obj, 'key', { value: 'value5' }) 
console.log(obj)

// Unavailable
Object.defineProperty(obj, 'key', { configurable: true })

// Unavailable
Object.defineProperty(obj, 'key', { writable: true })

// Output:
//   { key: 'value' }
//   { key: 'value1' }
//   { key: 'value2' }
//   { key: 'value3' }
//   TypeError: Cannot assign to read only property 'key' of object '#<Object>'
//   TypeError: Cannot redefine property: key
//   TypeError: Cannot redefine property: key
//   TypeError: Cannot redefine property: key

關於 descriptor 裡的 enumerableconfigurablewritable, value 這四種特性大致研究到這邊。明天再來繼續研究 get()set() ,以及 Object.defineProperty() 特別適合運用在哪些情境中吧。


1: properties 和 attributes 在英中翻譯上通常都會翻成屬性,由於本文兩者都有出現,所以在翻譯上會特別區別。Properties 取其有擁有(own)之意,且在這裡的確是特別指 Object 擁有的資料,所以維持「屬」性的翻譯;而這邊的 attributes 特別是指 descriptor 這個 object 裡的屬性,但這些屬性是會影響被敘述物件的行為,所以我會在換句話說時講成設定,或者是採用「特性」作為翻譯。


上一篇
前言
下一篇
JS Object.defineProperty(): (2) getter & setter
系列文
我或許沒那麼懂 Web31

尚未有邦友留言

立即登入留言