Object.defineProperty()函數(shù)之屬性描述對象
概述
JavaScript 提供了一個內(nèi)部數(shù)據(jù)結(jié)構(gòu),用來描述對象的屬性,控制它的行為,比如該屬性是否可寫、可遍歷等等。這個內(nèi)部數(shù)據(jù)結(jié)構(gòu)稱為“屬性描述對象”(attributes object)。每個屬性都有自己對應(yīng)的屬性描述對象,保存該屬性的一些元信息。
下面是屬性描述對象的一個例子:
{
value: 123,
writable: false,
enumerable: true,
configurable: false,
get: undefined,
set: undefined
}
屬性描述對象提供6個元屬性。
(1)value
value是該屬性的屬性值,默認(rèn)為undefined。
(2)writable
writable是一個布爾值,表示屬性值(value)是否可改變(即是否可寫),默認(rèn)為true。
(3)enumerable
enumerable是一個布爾值,表示該屬性是否可遍歷,默認(rèn)為true。如果設(shè)為false,會使得某些操作(比如for...in循環(huán)、Object.keys())跳過該屬性。
(4)configurable
configurable是一個布爾值,表示可配置性,默認(rèn)為true。如果設(shè)為false,將阻止某些操作改寫該屬性,比如無法刪除該屬性,也不得改變該屬性的屬性描述對象(value屬性除外)。也就是說,configurable屬性控制了屬性描述對象的可寫性。
(5)get
get是一個函數(shù),表示該屬性的取值函數(shù)(getter),默認(rèn)為undefined。
(6)set
set是一個函數(shù),表示該屬性的存值函數(shù)(setter),默認(rèn)為undefined。
Object.getOwnPropertyDescriptor()
Object.getOwnPropertyDescriptor方法可以獲取屬性描述對象。它的第一個參數(shù)是一個對象,第二個參數(shù)是一個字符串,對應(yīng)該對象的某個屬性名。
var obj = { p: 'a' };
Object.getOwnPropertyDescriptor(obj, 'p')
// Object { value: "a",
// writable: true,
// enumerable: true,
// configurable: true
// }上面代碼中,Object.getOwnPropertyDescriptor方法獲取obj.p的屬性描述對象。
注意:Object.getOwnPropertyDescriptor方法只能用于對象自身的屬性,不能用于繼承的屬性。
var obj = { p: 'a' };
Object.getOwnPropertyDescriptor(obj, 'toString')
// undefined上面代碼中,toString是Obj對象繼承的屬性,Object.getOwnPropertyDescriptor無法獲取。
Object.getOwnPropertyNames()
Object.getOwnPropertyNames方法返回一個數(shù)組,成員是參數(shù)對象自身的全部屬性的屬性名,不管該屬性是否可遍歷。
var obj = Object.defineProperties({}, {
p1: { value: 1, enumerable: true },
p2: { value: 2, enumerable: false }
});
Object.getOwnPropertyNames(obj)
// ["p1", "p2"]上面代碼中,obj.p1是可遍歷的,obj.p2是不可遍歷的。Object.getOwnPropertyNames會將它們都返回。
這跟Object.keys的行為不同,Object.keys只返回對象自身的可遍歷屬性的全部屬性名。
Object.keys([]) // [] Object.getOwnPropertyNames([]) // [ 'length' ] Object.keys(Object.prototype) // [] Object.getOwnPropertyNames(Object.prototype) // ['hasOwnProperty', // 'valueOf', // 'constructor', // 'toLocaleString', // 'isPrototypeOf', // 'propertyIsEnumerable', // 'toString']
上面代碼中,數(shù)組自身的length屬性是不可遍歷的,Object.keys不會返回該屬性。第二個例子的Object.prototype也是一個對象,所有實例對象都會繼承它,它自身的屬性都是不可遍歷的。
Object.defineProperty(),Object.defineProperties()
Object.defineProperty方法允許通過屬性描述對象,定義或修改一個屬性,然后返回修改后的對象,它的用法如下。
Object.defineProperty(object, propertyName, attributesObject)
Object.defineProperty方法接受三個參數(shù),
依次如下:
- 屬性所在的對象
- 屬性名(它應(yīng)該是一個字符串)
- 屬性描述對象
舉例來說,定義obj.p可以寫成下面這樣。
var obj = Object.defineProperty({}, 'p', {
value: 123,
writable: false,
enumerable: true,
configurable: false
});
obj.p // 123
obj.p = 246;
obj.p // 123上面代碼中,Object.defineProperty方法定義了obj.p屬性。由于屬性描述對象的writable屬性為false,所以obj.p屬性不可寫。注意,這里的Object.defineProperty方法的第一個參數(shù)是{}(一個新建的空對象),p屬性直接定義在這個空對象上面,然后返回這個對象,這是Object.defineProperty的常見寫法。
如果屬性已經(jīng)存在,Object.defineProperty方法相當(dāng)于更新該屬性的屬性描述對象。
如果一次性定義或修改多個屬性,可以使用Object.defineProperties方法。
var obj = Object.defineProperties({}, {
p1: { value: 123, enumerable: true },
p2: { value: 'abc', enumerable: true },
p3: { get: function () { return this.p1 + this.p2 },
enumerable:true,
configurable:true
}
});
obj.p1 // 123
obj.p2 // "abc"
obj.p3 // "123abc"上面代碼中,Object.defineProperties同時定義了obj對象的三個屬性。其中,p3屬性定義了取值函數(shù)get,即每次讀取該屬性,都會調(diào)用這個取值函數(shù)。
注意,一旦定義了取值函數(shù)get(或存值函數(shù)set),就不能將writable屬性設(shè)為true,或者同時定義value屬性,否則會報錯。
var obj = {};
Object.defineProperty(obj, 'p', {
value: 123,
get: function() { return 456; }
});
// TypeError: Invalid property.
// A property cannot both have accessors and be writable or have a value
Object.defineProperty(obj, 'p', {
writable: true,
get: function() { return 456; }
});
// TypeError: Invalid property descriptor.
// Cannot both specify accessors and a value or writable attribute上面代碼中,同時定義了get屬性和value屬性,以及將writable屬性設(shè)為true,就會報錯。
Object.defineProperty()和Object.defineProperties()的第三個參數(shù),是一個屬性對象。它的writable、configurable、enumerable這三個屬性的默認(rèn)值都為false。
var obj = {};
Object.defineProperty(obj, 'foo', {});
Object.getOwnPropertyDescriptor(obj, 'foo')
// {
// value: undefined,
// writable: false,
// enumerable: false,
// configurable: false
// }上面代碼中,定義obj.p時用了一個空的屬性描述對象,就可以看到各個元屬性的默認(rèn)值。
Object.prototype.propertyIsEnumerable()
實例對象的propertyIsEnumerable方法返回一個布爾值,用來判斷某個屬性是否可遍歷。
var obj = {};
obj.p = 123;
obj.propertyIsEnumerable('p') // true
obj.propertyIsEnumerable('toString') // false上面代碼中,obj.p是可遍歷的,而繼承自原型對象的obj.toString屬性是不可遍歷的。
元屬性
屬性描述對象的各個屬性稱為“元屬性”,因為它們可以看作是控制屬性的屬性。
value
value屬性是目標(biāo)屬性的值。
var obj = {};
obj.p = 123;
Object.getOwnPropertyDescriptor(obj, 'p').value
// 123
Object.defineProperty(obj, 'p', { value: 246 });
obj.p // 246上面代碼是通過value屬性,讀取或改寫obj.p的例子。
writable
writable屬性是一個布爾值,決定了目標(biāo)屬性的值(value)是否可以被改變。
var obj = {};
Object.defineProperty(obj, 'a', {
value: 37,
writable: false
});
obj.a // 37
obj.a = 25;
obj.a // 37上面代碼中,obj.a的writable屬性是false。然后,改變obj.a的值,不會有任何效果。
注意:正常模式下,對writable為false的屬性賦值不會報錯,只會默默失敗。但是,嚴(yán)格模式下會報錯,即使對a屬性重新賦予一個同樣的值。
'use strict';
var obj = {};
Object.defineProperty(obj, 'a', {
value: 37,
writable: false
});
obj.a = 37;
// Uncaught TypeError: Cannot assign to read only property 'a' of object上面代碼是嚴(yán)格模式,對obj.a任何賦值行為都會報錯。
如果原型對象的某個屬性的writable為false,那么子對象將無法自定義這個屬性。
var proto = Object.defineProperty({}, 'foo', {
value: 'a',
writable: false
});
var obj = Object.create(proto);
obj.foo = 'b';
obj.foo // 'a'上面代碼中,proto是原型對象,它的foo屬性不可寫。obj對象繼承proto,也不可以再自定義這個屬性了。如果是嚴(yán)格模式,這樣做還會拋出一個錯誤。
但是,有一個規(guī)避方法,就是通過覆蓋屬性描述對象,繞過這個限制。原因是這種情況下,原型鏈會被完全忽視。
var proto = Object.defineProperty({}, 'foo', {
value: 'a',
writable: false
});
var obj = Object.create(proto);
Object.defineProperty(obj, 'foo', {
value: 'b'
});
obj.foo // "b"enumerable
enumerable(可遍歷性)返回一個布爾值,表示目標(biāo)屬性是否可遍歷。
JavaScript 的早期版本,for...in循環(huán)是基于in運算符的。我們知道,in運算符不管某個屬性是對象自身的還是繼承的,都會返回true。
var obj = {};
'toString' in obj // true上面代碼中,toString不是obj對象自身的屬性,但是in運算符也返回true,這導(dǎo)致了toString屬性也會被for...in循環(huán)遍歷。
這顯然不太合理,后來就引入了“可遍歷性”這個概念。只有可遍歷的屬性,才會被for...in循環(huán)遍歷,同時還規(guī)定toString這一類實例對象繼承的原生屬性,都是不可遍歷的,這樣就保證了for...in循環(huán)的可用性。
具體來說,如果一個屬性的enumerable為false,
下面三個操作不會取到該屬性:
for..in循環(huán)Object.keys方法JSON.stringify方法
因此,enumerable可以用來設(shè)置“秘密”屬性。
var obj = {};
Object.defineProperty(obj, 'x', {
value: 123,
enumerable: false
});
obj.x // 123
for (var key in obj) {
console.log(key);
}
// undefined
Object.keys(obj) // []
JSON.stringify(obj) // "{}"上面代碼中,obj.x屬性的enumerable為false,所以一般的遍歷操作都無法獲取該屬性,使得它有點像“秘密”屬性,但不是真正的私有屬性,還是可以直接獲取它的值。
注意:for...in循環(huán)包括繼承的屬性,Object.keys方法不包括繼承的屬性。如果需要獲取對象自身的所有屬性,不管是否可遍歷,可以使用Object.getOwnPropertyNames方法。
另外,JSON.stringify方法會排除enumerable為false的屬性,有時可以利用這一點。如果對象的 JSON 格式輸出要排除某些屬性,就可以把這些屬性的enumerable設(shè)為false。
configurable
configurable(可配置性)返回一個布爾值,決定了是否可以修改屬性描述對象。也就是說,configurable為false時,value、writable、enumerable和configurable都不能被修改了。
var obj = Object.defineProperty({}, 'p', {
value: 1,
writable: false,
enumerable: false,
configurable: false
});
Object.defineProperty(obj, 'p', {value: 2})
// TypeError: Cannot redefine property: p
Object.defineProperty(obj, 'p', {writable: true})
// TypeError: Cannot redefine property: p
Object.defineProperty(obj, 'p', {enumerable: true})
// TypeError: Cannot redefine property: p
Object.defineProperty(obj, 'p', {configurable: true})
// TypeError: Cannot redefine property: p上面代碼中,obj.p的configurable為false。然后,改動value、writable、enumerable、configurable,結(jié)果都報錯。
注意:writable只有在false改為true會報錯,true改為false是允許的。
var obj = Object.defineProperty({}, 'p', {
writable: true,
configurable: false
});
Object.defineProperty(obj, 'p', {writable: false})
// 修改成功至于value,只要writable和configurable有一個為true,就允許改動。
var o1 = Object.defineProperty({}, 'p', {
value: 1,
writable: true,
configurable: false
});
Object.defineProperty(o1, 'p', {value: 2})
// 修改成功
var o2 = Object.defineProperty({}, 'p', {
value: 1,
writable: false,
configurable: true
});
Object.defineProperty(o2, 'p', {value: 2})
// 修改成功另外,configurable為false時,直接目標(biāo)屬性賦值,不報錯,但不會成功。
var obj = Object.defineProperty({}, 'p', {
value: 1,
configurable: false
});
obj.p = 2;
obj.p // 1上面代碼中,obj.p的configurable為false,對obj.p賦值是不會生效的。如果是嚴(yán)格模式,還會報錯。
可配置性決定了目標(biāo)屬性是否可以被刪除(delete)。
var obj = Object.defineProperties({}, {
p1: { value: 1, configurable: true },
p2: { value: 2, configurable: false }
});
delete obj.p1 // true
delete obj.p2 // false
obj.p1 // undefined
obj.p2 // 2上面代碼中,obj.p1的configurable是true,所以可以被刪除,obj.p2就無法刪除。
存取器
除了直接定義以外,屬性還可以用存取器(accessor)定義。其中,存值函數(shù)稱為setter,使用屬性描述對象的set屬性;取值函數(shù)稱為getter,使用屬性描述對象的get屬性。
一旦對目標(biāo)屬性定義了存取器,那么存取的時候,都將執(zhí)行對應(yīng)的函數(shù)。利用這個功能,可以實現(xiàn)許多高級特性,比如某個屬性禁止賦值。
var obj = Object.defineProperty({}, 'p', {
get: function () {
return 'getter';
},
set: function (value) {
console.log('setter: ' + value);
}
});
obj.p // "getter"
obj.p = 123 // "setter: 123"上面代碼中,obj.p定義了get和set屬性。obj.p取值時,就會調(diào)用get;賦值時,就會調(diào)用set。
JavaScript 還提供了存取器的另一種寫法。
var obj = {
get p() {
return 'getter';
},
set p(value) {
console.log('setter: ' + value);
}
};上面的寫法與定義屬性描述對象是等價的,而且使用更廣泛。
注意,取值函數(shù)get不能接受參數(shù),存值函數(shù)set只能接受一個參數(shù)(即屬性的值)。
存取器往往用于,屬性的值依賴對象內(nèi)部數(shù)據(jù)的場合。
var obj ={
$n : 5,
get next() { return this.$n++ },
set next(n) {
if (n >= this.$n) this.$n = n;
else throw new Error('新的值必須大于當(dāng)前值');
}
};
obj.next // 5
obj.next = 10;
obj.next // 10
obj.next = 5;
// Uncaught Error: 新的值必須大于當(dāng)前值上面代碼中,next屬性的存值函數(shù)和取值函數(shù),都依賴于內(nèi)部屬性$n。
對象的拷貝
有時,我們需要將一個對象的所有屬性,拷貝到另一個對象,可以用下面的方法實現(xiàn)。
var extend = function (to, from) {
for (var property in from) {
to[property] = from[property];
}
return to;
}
extend({}, {
a: 1
})
// {a: 1}上面這個方法的問題在于,如果遇到存取器定義的屬性,會只拷貝值。
extend({}, {
get a() { return 1 }
})
// {a: 1}為了解決這個問題,我們可以通過Object.defineProperty方法來拷貝屬性。
var extend = function (to, from) {
for (var property in from) {
if (!from.hasOwnProperty(property)) continue;
Object.defineProperty(
to,
property,
Object.getOwnPropertyDescriptor(from, property)
);
}
return to;
}
extend({}, { get a(){ return 1 } })
// { get a(){ return 1 } })上面代碼中,hasOwnProperty那一行用來過濾掉繼承的屬性,否則會報錯,因為Object.getOwnPropertyDescriptor讀不到繼承屬性的屬性描述對象。
控制對象狀態(tài)
有時需要凍結(jié)對象的讀寫狀態(tài),防止對象被改變。JavaScript 提供了三種凍結(jié)方法,最弱的一種是Object.preventExtensions,其次是Object.seal,最強的是Object.freeze。
Object.preventExtensions()
Object.preventExtensions方法可以使得一個對象無法再添加新的屬性。
var obj = new Object();
Object.preventExtensions(obj);
Object.defineProperty(obj, 'p', {
value: 'hello'
});
// TypeError: Cannot define property:p, object is not extensible.
obj.p = 1;
obj.p // undefined上面代碼中,obj對象經(jīng)過Object.preventExtensions以后,就無法添加新屬性了。
Object.isExtensible()
Object.isExtensible方法用于檢查一個對象是否使用了Object.preventExtensions方法。也就是說,檢查是否可以為一個對象添加屬性。
var obj = new Object(); Object.isExtensible(obj) // true Object.preventExtensions(obj); Object.isExtensible(obj) // false
上面代碼中,對obj對象使用Object.preventExtensions方法以后,再使用Object.isExtensible方法,返回false,表示已經(jīng)不能添加新屬性了。
Object.seal()
Object.seal方法使得一個對象既無法添加新屬性,也無法刪除舊屬性。
var obj = { p: 'hello' };
Object.seal(obj);
delete obj.p;
obj.p // "hello"
obj.x = 'world';
obj.x // undefined上面代碼中,obj對象執(zhí)行Object.seal方法以后,就無法添加新屬性和刪除舊屬性了。
Object.seal實質(zhì)是把屬性描述對象的configurable屬性設(shè)為false,因此屬性描述對象不再能改變了。
var obj = {
p: 'a'
};
// seal方法之前
Object.getOwnPropertyDescriptor(obj, 'p')
// Object {
// value: "a",
// writable: true,
// enumerable: true,
// configurable: true
// }
Object.seal(obj);
// seal方法之后
Object.getOwnPropertyDescriptor(obj, 'p')
// Object {
// value: "a",
// writable: true,
// enumerable: true,
// configurable: false
// }
Object.defineProperty(o, 'p', {
enumerable: false
})
// TypeError: Cannot redefine property: p上面代碼中,使用Object.seal方法之后,屬性描述對象的configurable屬性就變成了false,然后改變enumerable屬性就會報錯。
Object.seal只是禁止新增或刪除屬性,并不影響修改某個屬性的值。
var obj = { p: 'a' };
Object.seal(obj);
obj.p = 'b';
obj.p // 'b'上面代碼中,Object.seal方法對p屬性的value無效,是因為此時p屬性的可寫性由writable決定。
Object.isSealed()
Object.isSealed方法用于檢查一個對象是否使用了Object.seal方法。
var obj = { p: 'a' };
Object.seal(obj);
Object.isSealed(obj) // true這時,Object.isExtensible方法也返回false。
var obj = { p: 'a' };
Object.seal(obj);
Object.isExtensible(obj) // falseObject.freeze()
Object.freeze方法可以使得一個對象無法添加新屬性、無法刪除舊屬性、也無法改變屬性的值,使得這個對象實際上變成了常量。
var obj = {
p: 'hello'
};
Object.freeze(obj);
obj.p = 'world';
obj.p // "hello"
obj.t = 'hello';
obj.t // undefined
delete obj.p // false
obj.p // "hello"上面代碼中,對obj對象進行Object.freeze()以后,修改屬性、新增屬性、刪除屬性都無效了。這些操作并不報錯,只是默默地失敗。如果在嚴(yán)格模式下,則會報錯。
Object.isFrozen()
Object.isFrozen方法用于檢查一個對象是否使用了Object.freeze方法。
var obj = {
p: 'hello'
};
Object.freeze(obj);
Object.isFrozen(obj) // true使用Object.freeze方法以后,Object.isSealed將會返回true,Object.isExtensible返回false。
var obj = {
p: 'hello'
};
Object.freeze(obj);
Object.isSealed(obj) // true
Object.isExtensible(obj) // falseObject.isFrozen的一個用途是,確認(rèn)某個對象沒有被凍結(jié)后,再對它的屬性賦值。
var obj = {
p: 'hello'
};
Object.freeze(obj);
if (!Object.isFrozen(obj)) {
obj.p = 'world';
}上面代碼中,確認(rèn)obj沒有被凍結(jié)后,再對它的屬性賦值,就不會報錯了。
局限性
上面的三個方法鎖定對象的可寫性有一個漏洞:可以通過改變原型對象,來為對象增加屬性。
var obj = new Object(); Object.preventExtensions(obj); var proto = Object.getPrototypeOf(obj); proto.t = 'hello'; obj.t // hello
上面代碼中,對象obj本身不能新增屬性,但是可以在它的原型對象上新增屬性,就依然能夠在obj上讀到。
一種解決方案是,把obj的原型也凍結(jié)住。
var obj = new Object(); Object.preventExtensions(obj); var proto = Object.getPrototypeOf(obj); Object.preventExtensions(proto); proto.t = 'hello'; obj.t // undefined
另外一個局限是,如果屬性值是對象,上面這些方法只能凍結(jié)屬性指向的對象,而不能凍結(jié)對象本身的內(nèi)容。
var obj = {
foo: 1,
bar: ['a', 'b']
};
Object.freeze(obj);
obj.bar.push('c');
obj.bar // ["a", "b", "c"]上面代碼中,obj.bar屬性指向一個數(shù)組,obj對象被凍結(jié)以后,這個指向無法改變,即無法指向其他值,但是所指向的數(shù)組是可以改變的。
到此這篇關(guān)于Object.defineProperty()函數(shù)之屬性描述對象的文章就介紹到這了,更多相關(guān)Object.defineProperty函數(shù)內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
JavaScript實現(xiàn)鼠標(biāo)經(jīng)過表格行給出顏色標(biāo)識
這篇文章主要為大家詳細(xì)介紹了JavaScript實現(xiàn)鼠標(biāo)經(jīng)過表格行給出顏色標(biāo)識,文中示例代碼介紹的非常詳細(xì),具有一定的參考價值,感興趣的小伙伴們可以參考一下2021-04-04
阿里巴巴技術(shù)文章分享 Javascript繼承機制的實現(xiàn)
這篇文章主要介紹了阿里巴巴技術(shù)文章,分享的是一篇關(guān)于Javascript實現(xiàn)繼承機制的文章,感興趣的小伙伴們可以參考一下2016-01-01
如何實現(xiàn)JavaScript動態(tài)加載CSS和JS文件
這篇文章主要為大家詳細(xì)介紹了JavaScript動態(tài)加載CSS和JS文件,文中示例代碼介紹的非常詳細(xì),具有一定的參考價值,感興趣的小伙伴們可以參考一下2015-10-10

