物件特性 API


在 JavaScript 中,物件基本上是鍵值的聚合體,你幾乎可以自由地修改物件,然而,如果你有個物件不想要被自由修改的話,則必須透過各種設計來限制相關特性。

ECMAScript 5 中對物件的特性(Properties)擴充或修改等提供了新的 API,特性本身也有了更豐富的描述,你仍然擁有修改物件的自由度,然而,在不需要這種自由度時,你也可以在嚴格模式之下加以限制。

限定物件的擴充

在 ECMAScript 5 中,提供了 Object.preventExtensionsObject.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 中物件上每個特性,都會有 valuewritableenumerableconfigurable 四個屬性:

  • value:特性的值。
  • writable:是否可修改特性值。
  • enumerable:是否可使用 for (var prop in obj) 迭代。
  • configurable:是否可刪除特性或修改特性的 writableconfigurableenumerable 屬性。

這幾個屬性合在一起,又稱為資料描述器(Data descriptor),為特性描述器的一部份,可以使用 Object.getOwnPropertyDescriptor 來取得特性描述器的資訊。

如果你直接於物件上新增特性,那麼 writableenumerableconfigurable 預設都會是 true。例如:

var obj = {name : 'caterpillar'};
console.log(JSON.stringify(
    Object.getOwnPropertyDescriptor(obj, 'name')
));

上面這個範例執行之後,會顯示 {“value”:“caterpillar”,“writable”:true,“enumerable”:true,“configurable”:true}。Object.getOwnPropertyDescriptor 只是用來取得特性描述器的資訊,而不是特性描述器本身,你對它傳回的物件進行修改是沒有作用的,想要修改特性描述器本身,必須透過 Object.definePropertyObject.defineProperties

Object.definePropertyObject.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 定義特性時,如果特性的屬性先前都沒有值,那麼 writableenumerableconfigurable 屬性值預設都會是 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},除了 enumerablefalse 外,其他都是 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.definePropertyObject.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
}

注意,如果你定義了 getset,表示你要自行控制特性的存取,也就是說,你就不能再去定義 valuewritable 特性。

如果物件被 Object.preventExtensions 標示為無法擴充,對該物件使用 Object.definePropertyObject.defineProperties 會引發 TypeError

sealfreeze

基於 Object.preventExtensionsObject.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);
};