在 JavaScript 中,物件基本上是鍵值的聚合體,你幾乎可以自由地修改物件,然而,如果你有個物件不想要被自由修改的話,則必須透過各種設計來限制相關特性。
ECMAScript 5 中對物件的特性(Properties)擴充或修改等提供了新的 API,特性本身也有了更豐富的描述,你仍然擁有修改物件的自由度,然而,在不需要這種自由度時,你也可以在嚴格模式之下加以限制。
限定物件的擴充
在 ECMAScript 5 中,提供了 Object.preventExtensions
與 Object.isExtensible
,可讓你限定或測試物件的擴充性。Object.preventExtensions
可指定物件,將物件標示為無法直接擴充並傳回物件本身,可透過 Object.isExtensible
測試物件是否可直接擴充,從呼叫的 Object.preventExtensions
時間點之後,對物件進行任何直接擴充,在嚴格模式下會引發 TypeError
。例如:
var obj1 = {};
console.log(Object.isExtensible(obj1)); // true
obj1.name = 'caterpillar';
var obj2 = Object.preventExtensions(obj1);
console.log(obj1 === obj2); // true
console.log(Object.isExtensible(obj1)); // false
obj1.age = 39; // TypeError
被標示為無法擴充的物件,只是無法再增添特性,不過仍然可以用 delete
刪除特性,也可以對特性加以修改,而且你只是無法對物件直接進行擴充,然而對於建構式 prototype
的擴充,仍然會被物件繼承下來。例如:
var obj = {name : 'caterpillar'};
Object.preventExtensions(obj);
obj.name = 'Justin';
console.log(obj.name); // Justin
delete obj.name;
console.log(obj.name); // undefined
Object.prototype.name = 'caterpillar';
console.log(obj.name); // caterpillar
在 ECMAScript 5 中,物件一但被 Object.preventExtensions
標示為無法擴充,沒有方式可以將之重設為可擴充。
特性描述器
想要進一步限定物件的特性可否修改、刪除等,必須透過 ECMAScript 5 新增的其他 API,不過在這之前,你必須認識 ECMAScript 5 中定義的特性描述器(Property descriptor)。
不同於過去物件上的特性,單純只是一對名稱與值,ECMAScript 5 中物件上每個特性,都會有 value
、writable
、 enumerable
與 configurable
四個屬性:
value
:特性的值。writable
:是否可修改特性值。enumerable
:是否可使用for (var prop in obj)
迭代。configurable
:是否可刪除特性或修改特性的writable
、configurable
與enumerable
屬性。
這幾個屬性合在一起,又稱為資料描述器(Data descriptor),為特性描述器的一部份,可以使用 Object.getOwnPropertyDescriptor
來取得特性描述器的資訊。
如果你直接於物件上新增特性,那麼 writable
、 enumerable
與 configurable
預設都會是 true
。例如:
var obj = {name : 'caterpillar'};
console.log(JSON.stringify(
Object.getOwnPropertyDescriptor(obj, 'name')
));
上面這個範例執行之後,會顯示 {“value”:“caterpillar”,“writable”:true,“enumerable”:true,“configurable”:true}。Object.getOwnPropertyDescriptor
只是用來取得特性描述器的資訊,而不是特性描述器本身,你對它傳回的物件進行修改是沒有作用的,想要修改特性描述器本身,必須透過 Object.defineProperty
或 Object.defineProperties
。
Object.defineProperty
、Object.defineProperties
你可以使用 Object.defineProperty
來定義特性名稱,以及特性描述器各屬性的值。例如:
var obj = {};
Object.defineProperty(obj, 'name', {
value : 'caterpillar',
writable : false,
enumerable : false,
configurable : false
});
console.log(JSON.stringify(
Object.getOwnPropertyDescriptor(obj, 'name')
));
執行以上範例,會顯示 {“value”:“caterpillar”,“writable”:false,“enumerable”:false,“configurable”:false}。
事實上,如果你使用 Object.defineProperty
定義特性時,如果特性的屬性先前都沒有值,那麼 writable
、 enumerable
或 configurable
屬性值預設都會是 false
,因此以下也是顯示 {“value”:“caterpillar”,“writable”:false,“enumerable”:false,“configurable”:false}。
var obj = {};
Object.defineProperty(obj, 'name', {
value : 'caterpillar'
});
console.log(JSON.stringify(
Object.getOwnPropertyDescriptor(obj, 'name')
));
然而,以下會顯示 {“value”:“caterpillar”,“writable”:true,“enumerable”:false,“configurable”:true},除了 enumerable
為 false
外,其他都是 true
。
var obj = {name : 'caterpillar'};
Object.defineProperty(obj, 'name', {
enumerable: false
});
console.log(JSON.stringify(
Object.getOwnPropertyDescriptor(obj, 'name')
));
如果有多個特性要設定,可以使用 Object.defineProperties
,例如:
var obj = {};
Object.defineProperties(obj, {
'name': {
value : 'John',
enumerable : true
},
'age': {
value : 39,
writable : true,
enumerable : true
},
});
如果特性的 writable
屬性為 false
時,嚴格模式下重新設定特性的值會引發 TypeError
,如果 configurable
屬性為 false
時,嚴格模式下刪除特性會引發 TypeError
。
實際上,你還可以使用 Object.defineProperty
、Object.defineProperties
來定義特性的存取描述器(Accessor descriptor),這也是特性描述器的一部份,可對特性進行進一步存取控制,例如,用來實現以下的封裝風格:
var obj = {};
Object.defineProperty(obj, 'name', {
get : function(){ return this.__name__; },
set : function(value){ this.__name__ = value.trim(); },
enumerable : true
});
Object.defineProperty(obj, '__name__', {
writable : true,
enumerable : false
});
obj.name = ' Justin ';
console.log('*' + obj.name + '*'); // *Justin*
for(var p in obj) {
console.log(p); // 只會顯示 name
}
注意,如果你定義了 get
、set
,表示你要自行控制特性的存取,也就是說,你就不能再去定義 value
或 writable
特性。
如果物件被 Object.preventExtensions
標示為無法擴充,對該物件使用 Object.defineProperty
、Object.defineProperties
會引發 TypeError
。
seal
與 freeze
基於 Object.preventExtensions
、Object.defineProperty
等 API,ECMAScrpt 5 還定義了 Object.seal
,可以讓你對物件加以彌封,被彌封的物件不能擴充或刪除物件上的特性,也不能修改特性描述器,但可以修改現有的特性值,可以使用 Object.isSeal
來測試物件是否被彌封,如果自行實作個 Object.seal
,大概是以下的方式:
Object.seal = function(obj) {
Object.getOwnPropertyNames(obj)
.forEach(function(prop) {
var desc = Object.getOwnPropertyDescriptor(obj, prop);
desc.configurable = false;
Object.defineProperty(obj, prop, desc);
});
return Object.preventExtensions(obj);
};
被彌封的物件,仍然可以修改現有的特性值,如果連特性值都不能被修改,只想作為一個唯讀物件,那麼可以使用 Object.freeze
來凍結物件,可以使用 Object.isFrozen
來測試物件是否被凍結。
Object.freeze = function(obj) {
Object.getOwnPropertyNames(obj)
.forEach(function(prop) {
var desc = Object.getOwnPropertyDescriptor(obj, prop);
if('value' in desc) { // 排除設定了 get 與 set 的情況
desc.writable = false;
}
desc.configurable = false;
Object.defineProperty(obj, prop, desc);
});
return Object.preventExtensions(obj);
};