1. 程式人生 > >Object.defineProperty()

Object.defineProperty()

而且 存檔 enum 屬性 ring stack .get code delet

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

對象定義屬性和賦值

在對象中,我們有很多種方式給其定義屬性和賦值。最常見的是obj.prop = valueobj[‘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中有三種類型的屬性:

  • 命名數據屬性:擁有一個確定的值的屬性。這也是最常見的屬性
  • 命名訪問器屬性:通過gettersetter進行讀取和賦值的屬性
  • 內部屬性:由JavaScript引擎內部使用的屬性,不能通過JavaScript代碼直接訪問到,不過可以通過一些方法間接的讀取和設置。比如,每個對象都有一個內部屬性[[Prototype]]
    ,你不能直接訪問這個屬性,但可以通過Object.getPrototypeOf()方法間接的讀取到它的值。雖然內部屬性通常用一個雙呂括號包圍的名稱來表示,但實際上這並不是它們的名字,它們是一種抽象操作,是不可見的,根本沒有上面兩種屬性有的那種字符串類型的屬性名

屬性特性

對象中每個屬性都有四個特性。兩種類型的屬性一共有六個屬性特性:

  • 命名數據屬性特有的特性:屬性的值的[[Value]]特性和控制屬性的值是否可以修改的[[Writable]]特性
  • 命名訪問器屬性特有的特性:存儲著getter方法的[[Get]]和存儲著setter方法的[[Set]]
  • 兩種屬性都有的特性:如果一個屬性是不可枚舉的,則一些操作下,這個屬性是不可見的,比如for...inObject.keys,那麽可以通過[[Enumerable]]特性來設置;如果一個屬性是不可配置的,則該屬性的所有特性([[Value]])都不可改變,那麽可以通過[[Configurable]]特性來設置

內部屬性

除了上面所說的之外,下面幾個是所有對象都有的內部屬性:

  • [[Prototype]]:對象的原型
  • [[Extensible]]:對象是否可擴展,也就是是否可以添加新的屬性
  • [[DefineOwnProperty]]:定義一個屬性的內部方法
  • [[Put]]:為一個屬性賦值的內部方法

屬性描述符

對象裏目前存在的屬性描述符有兩種主要形式:數據描述符存取描述符

  • 數據描述符:是一個擁有可寫或不可寫值的屬性
  • 存取描述符:是由一對 gettersetter 函數功能來描述的屬性

描述符必須是兩種形式之一;不能同時是兩者。另外,屬性描述符可以將一個屬性的所有特性編碼成一個對象並返回。例如:

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的描述符,首先來看writabletrue的情況:

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:僅當該屬性的enumerabletrue時,該屬性才能夠出現在對象的枚舉屬性中。默認為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修飾符重置為truename屬性才可以被修改。

writable:僅當該屬性的writabletrue時,該屬性的屬性值才能被改變。默認為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決定了在定義操作被拒絕的時候是否要拋出異常。如果Throwtrue,則拋出異常;否則,操作只會靜默失敗。當調用[[DefineOwnProperty]]時,具體會執行下面的操作步驟。

  • 如果this沒有名為P的自身屬性的話:如果this是可擴展的話,則創建P這個自身屬性,否則拒絕
  • 如果this已經有了名為P的自身屬性:則按照下面的步驟重新配置這個屬性
  • 如果這個已有的屬性是不可配置的,則進行下面的操作會被拒絕:將一個數據屬性轉換成訪問器屬性,反之變然;改變[[Configurable]][[Enumerable]];改變[[Writable]];在[[Writable]]false時改變[[Value]]和改變[[Get]][[Set]]
  • 否則,這個已有的屬性可以被重新配置

如果Desc就是P屬性當前的屬性描述符,則該定義操作永遠不會被拒絕。

定義屬性的函數有兩個:Object.definePropertyObject.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屬性和為objfoo屬性賦值有什麽區別呢?

如果是定義操作的話,則會在obj身上添加一個自身屬性foo

Object.defineProperty(obj, ‘foo‘, {
    value: ‘b‘
})

console.log(obj.foo)

console.log(proto.foo)

技術分享圖片

但如果為foo屬性賦值的話,則意味著你想改變某個已經存在的屬性的值。所以這次賦值操作會轉交給原型protofoo屬性的setter訪問器來處理。下面代碼的執行結果就能證明這一結論:

技術分享圖片

你還可以定義一個只讀的訪問器屬性,辦法是:只定義一個getter,省略setter。下面的例子中,protobar屬性就是這樣的只讀屬性,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()