对象特性 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:是否可删除特性或修改特性的 writable、configurable 与 enumerable 属性。

这几个属性合在一起,又称为数据描述器(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.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定义特性时,如果特性的属性先前都没有值,那么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

seal 与 freeze

基于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);
};




展开阅读全文