ES5规范之Object增强

在ES5规范中,还有一个比较重要的改进,就是Object对象的增强,ES5为Object新增了一系列函数,用于编写安全健壮的程序,今天我们就来一一介绍它们的使用方法。

下面就是ES5中Object新增的函数:

Object.defineProperty(object, propertyName, descriptor);

Object.defineProperties(object, descriptors);

Object.getOwnPropertyDescriptor(object, propertyName);

Object.create(prototype, descriptors);

Object.getPrototypeOf(object);

Object.keys(object);

Object.getOwnPropertyNames(object);

Object.preventExtensions(object);

Object.isExtensible(object);

Object.seal(object);

Object.isSealed(object);

Object.freeze(object);

Object.isFrozen(object);


下面我们逐一介绍:


Object.defineProperty(object, propertyName, descriptor);

defineProperty函数用于定义一个对象上的属性以及这个属性的描述符。这里涉及到描述符这个概念,需要先理解一下,描述符是用来描述一个属性的值和这个属性在运行期的访问控制,一个描述符包含下面几个声明:

configurable: 表示是否能通过delete删除对象中的该属性,以及是否能重新定义该属性,默认是false,默认情况下,不能用delete删除属性,不能重新定义该属性。需要注意的是,一旦明确设置configurable为false之后,就再也不能重新设置这个规则为true了。

enumerable: 表示能否通过for-in循环获得对象中的该属性,默认值是false,默认情况下,使用for-in循环将不能看到该属性出现。

writable: 表示能否修改该属性,默认值是false,默认情况下,不能再更改该属性的值。

value: 该属性的值,默认值是undefined。通常我们会设置value为一个有意义的值。

下面是每个声明的默认值列表:(Get和Set后续会介绍)

ES5规范之Object增强_第1张图片

这里需要注意,当configurablewritablefalse的情况下,试图删除更改相对应的属性,常规模式下操作将会被忽略,如果是严格模式,将会抛出异常。我们的代码会加上"use strict";来启用严格模式。关于严格模式的细节,我们后续的文章会专门介绍。

我们先来使用defineProperty函数定义一个对象的属性:

"use strict";

var person = {};

Object.defineProperty(person, 'name', {
  value: 'Scott'
});

person.name = 'John';
上面的代码我们使用defineProperty为person对象定义了一个name属性,然后试图更改它的值。如果在常规模式下运行,更改操作将会被忽略,这里我们加上了严格模式的声明,将会抛出下面的异常:

如果通过delete试图删除name属性,同样也会得到一个异常结果:

delete person.name;

我们稍微改动一下代码,为defineProperty函数的最后一个参数添加一个writable声明:

"use strict";

var person = {};

Object.defineProperty(person, 'name', {
  writable: true, //add writable as true
  value: 'Scott'
});

person.name = 'John';
console.log('after changing the name: ', person.name);

delete person.name;
console.log('after deleting the name: ', person.name);
添加writable为true后,再次运行程序,看看结果如何:

我们现在可以更改name属性的值了。只不过试图删除name时,仍然会抛出一个异常,现在我们需要再添加一个configurable声明:

"use strict";

var person = {};

Object.defineProperty(person, 'name', {
  configurable: true, //add configurable as true
  writable: true, //add writable as true
  value: 'Scott'
});

person.name = 'John';
console.log('after changing the name: ', person.name);

delete person.name;
console.log('after deleting the name: ', person.name);

ES5规范之Object增强_第2张图片

从打印结果来看,更改操作和删除操作在严格模式下都顺利执行了。现在大家应该都了解到configurable和writable的作用了吧。configurable允许或禁止删除操作的执行,writable允许或禁止更改操作的执行。

另外,上面我们也提到过,如果明确声明了configurable为false,则不能再使用defineProperty将其定义configurable为true了,下面代码将会抛出一个异常:

'use strict';

var person = {};

Object.defineProperty(person, 'name', {
  configurable: false,  //declare configurable as false
  writable: false,  //declare writable as false
  value: 'Scott'
});

//try to redefine the name's descriptor
Object.defineProperty(person, 'name', {
  configurable: true,
  writable: true,
  value: 'Scott'
});

当外部代码试图更改对象属性时,适当的使用configurable和writable为其加一些限制,可以提高代码的安全性,这一点对模块开发非常有用。

下面来介绍一下enumerable声明,我们用一个简单的示例来讲解:

"use strict";

var person = {};

Object.defineProperty(person, 'name', {
  value: 'Scott'
});

//nothing will be logged
for (var key in person) {
  if (person.hasOwnProperty(key)) {
    console.log(key + ': ' + person[key]);
  }
}
上面这段代码我们不会看到person中的键值对,因为默认情况下使用defineProperty定义属性时,属性的enumerable为false,即不可被遍历,如果需要在for-in中获取到name属性,我们需要为其声明enumerable为true:

"use strict";

var person = {};

Object.defineProperty(person, 'name', {
  enumerable: true, //add enumerable as true
  value: 'Scott'
});

//name: Scott
for (var key in person) {
  if (person.hasOwnProperty(key)) {
    console.log(key + ': ' + person[key]);
  }
}

检测一个对象的属性是否可遍历,我们还可以使用Object的另外一个原型方法,它更为方便:

var isNameEnumerable = person.propertyIsEnumerable('name');
除了上面这些之外,在defineProperty函数的descriptor中还可以使用setter和getter对属性进行定义:

var getPerson = function() {
  var person = {};
  
  var personAge = 20;

  Object.defineProperty(person, 'age', {
    get: function() {
      return personAge;
    },
    set: function(newAge) {
      if (newAge < 0) {
        newAge = 0;
      }
      if (newAge > 150) {
        newAge = 150
      }

      personAge = newAge;
    }
  });
  
  return person;
}

var person = getPerson();

person.age = -1;
console.log(person.age);  //0
person.age = 200;
console.log(person.age);  //150

上面代码中我们把创建person对象的操作封装在getPerson函数中,然后使用set方法和get方法定义age属性的行为,在set和get方法中,间接使用了局部变量personAge来表示person的age属性,这种方式的好处在于,可以对赋值操作加一些验证,使其符合我们业务逻辑的要求。不过需要注意的是,不是一定非要同时指定setter和getter的,在只定义getter时该属性不能写,只指定setter时不能读,如果只有getter而尝试去写、只有setter尝试去读的话,严格模式下会抛出异常,所以正确地使用setter和getter才能写出高质量的代码。


Object.defineProperties(object, descriptors);

在了解上面介绍的defineProperty函数之后,对于这个函数就比较容易理解了,defineProperties函数用于一次性定义多个属性,我们用一段代码来解释:

var person = {};

Object.defineProperties(person, {
  'name': {
    writable: false,
    value: 'Scott'
  },
  'address': {
    writable: true,
    value: 'Beijing'
  }
});

代码中我们最后一个参数包含两个属性:name和address,分别都有自己的属性描述信息,这种方式比较单一属性的声明来说简单明了,在一次性有多个属性需要声明时比较实用。


Object.getOwnPropertyDescriptor(object, propertyName);
这个函数用于获取指定属性的描述符,其中会包括上面提到的一些描述符声明,我们直接看下面示例代码:

var person = {};

Object.defineProperty(person, 'name', {
  configurable: true,
  writable: false,
  enumerable: true,
  value: 'Scott'
});

var descriptor = Object.getOwnPropertyDescriptor(person, 'name');
console.log(descriptor.configurable); //true
console.log(descriptor.writable); //false
console.log(descriptor.enumerable); //true
console.log(descriptor.value);  //Scott

需要注意的是,如果定义属性时使用了set和get方法,那么返回的descriptor对象也是可以通过set和get访问到相应的setter和getter的。

接着我们来了解一下与原型有关的两个函数:


Object.create(prototype, descriptors);

create函数用于在指定原型基础之上创建一个新的对象,第一个参数即指定的原型对象,第二个参数就是我们上面介绍到的描述符,里面可以定义多个属性的描述信息。原型参数只能是一个对象或者指定为null,下面三种方式都可以创建一个空对象:

var obj0 = Object.create({});
var obj1 = Object.create(null);
var obj2 = Object.create(Object.prototype);

我们当然还可以指定一个包含多个属性描述信息的描述符参数,来创建一个新的对象,在原来对象上进行扩展。下面这个例子我们在一个已有对象的基础之上创建一个person对象,包含name属性和gender属性,其中name可更改不可删除,gender是只读状态:

var human = {
  info: 'human being'
};

var person = Object.create(human, {
  name: {
    configurable: false,
    writable: true,
    enumerable: true,
    value: 'Scott'
  },
  gender: {
    get: function() {
      return "Male";
    }
  }
});

console.log(person);
上面代码相当于在human对象上进行扩展,添加了name和gender属性,进而创建一个新对象,我们来看一下person对象的结构:

ES5规范之Object增强_第3张图片

从打印信息中,我们可以看出,因为name属性是可遍历的,所以显示在Object{}中,当我们展开后,会看到gender的get方法,也可以看得到person的原型链,包含info属性的原型就是我们上面定义的human对象了,证明person对象确实是human对象的扩展。create与defineProperties有些相象之处,不同的是,create会创建一个新的对象,而defineProperties不会,我们也可以获取它的返回值,返回值就是原对象本身。


Object.getPrototypeOf(object);

此函数用于获取指定对象的原型,来看下面这个精简过的例子:

var human = {
  info: 'human being'
};

var person = Object.create(human, {
  name: {
    value: 'Scott'
  }
});

console.log(Object.getPrototypeOf(person) === human); //true
我们使用human作为原型创建一个person对象,然后使用getPrototypeOf函数获取person对象的原型,结果会返回human,我们上面也介绍到了,person对象的确是从human对象扩展而来,getPrototypeOf获取到的原型和我们期望的是一致的。

Object.keys(object);

此函数用于获取对象中可被遍历的全部属性的key的集合。看下面例子:

var array = ['a', 'b', 'c'];
console.log(Object.keys(array)); //["0", "1", "2"]

var person = Object.defineProperties({}, {
  name: {
    enumerable: true,
    value: 'Scott'
  },
  info: {
    enumerable: true,
    value: 'I am Scott'
  },
  address: {
    enumerable: false,  //not enumerable
    value: 'Beijing'
  }
});

console.log(Object.keys(person)); //["name", "info"]
首先我们创建一个数组,使用keys函数获取所有的key,结果会打印出数组的下标组成的集合;然后我们定义了person对象的属性,其中name和info都是可遍历的,address是不可遍历的,结果会打印出name和info,而不会出现address,使用keys函数的时候需要注意这一点。


Object.getOwnPropertyNames(object);

此函数与keys函数相似,不过它会返回所有的键,包括哪些不可被遍历的属性。现在我们把keys函数替换成getOwnPropertyNames,看看结果如何:

var array = ['a', 'b', 'c'];
console.log(Object.getOwnPropertyNames(array));

var person = Object.defineProperties({}, {
  name: {
    enumerable: true,
    value: 'Scott'
  },
  info: {
    enumerable: true,
    value: 'I am Scott'
  },
  address: {
    enumerable: false,  //not enumerable
    value: 'Beijing'
  }
});

console.log(Object.getOwnPropertyNames(person));
结果打印如下:

ES5规范之Object增强_第4张图片
我们会发现,array里面增加了一个length的key,而person对象也增加了一个address的key,我们可以得知,数组对象的length被设置为不可遍历的,也证明了getOwnPropertyNames确实可以把所有的属性的key获取到。


Object.preventExtensions(object); & Object.isExtensible(object);

preventExtensions函数用于禁止指定对象的扩展,即禁止向对象中添加新的属性。我们定义一个简单的对象,然后测试一下这个函数:

'use strict';

var person = {
  name: 'Scott'
};

Object.preventExtensions(person);

console.log(Object.isExtensible(person)); //false

//Uncaught TypeError: Can't add property age, object is not extensible
person.age = 20;
测试证明,试图添加一个age属性,严格模式下将会抛出异常,证明person对象已经是不可扩展的。

Object.seal(object); & Object.isSealed(object);

seal函数用于对指定对象进行封存,已封存的对象禁止再添加新的属性,禁止删除已有的属性,禁止重新定义已有属性的cnofigurable为true。来看下面一段代码:

'use strict';

var person = {
  name: 'Scott'
};

Object.seal(person);

console.log(Object.isSealed(person)); //true

person.name = 'John';

//Uncaught TypeError: Can't add property age, object is not extensible
//person.age = 20;

//Uncaught TypeError: Cannot delete property 'name' of #<Object>
delete person.name;

上面我们定义了一个普通字面量的person对象,它的name属性是可写可删除可遍历的,接着我们调用seal函数对其进行封存,如果我们想要判断一个对象是否已被封存,可以使用isSealed函数。被封存的对象禁止添加属性和删除已有的属性,所以我们试图添加一个age属性和删除已有的name属性都会在严格模式下抛出异常。需要注意的是,只要是可写的属性,即使被封存也可以更改其值,所以更改name的值是不会有问题的。

另外,对已被封存对象的属性重新定义也要特别小心,因为被封存的对像使用defineProperty重新定义规则时,只能更改writable和value,不能更改原来的enumerable,对于configurable更为严苛,不能明确声明。如下代码列举了一些注意事项:

'use strict';

var person = {
  name: 'Scott'
};

Object.seal(person);

person.name = 'Jack';
console.log(person.name); //Jack

//after sealed, can't declare the configurable, and can't change the original enumerable.
Object.defineProperty(person, 'name', {
  //configurable: true,
  writable: false,
  enumerable: true, //the enumerable here must be equal to the original if declare explicitly
  value: 'John'
});

//Uncaught TypeError: Cannot assign to read only property 'name' of object '#<Object>'
person.name = 'Scott';
可以看出writable是可以更改为false的,但注意enumerable只能和原来对象一样为true,而configurable在这里不能明确声明,大家可以亲自测试一下。


Object.freeze(object); & Object.isFrozen(object);

freeze函数用于对指定对象进行冻结,冻结后的对象禁止添加属性,禁止删除属性,禁止更改属性,禁止使用defineProperty更改其configurable,writable,和enumerable的值。可以说freeze函数禁止对象属性上的所有操作,不过需要注意的是,如果对象的属性也是对象类型,那么这个属性对象下面的属性是不会被冻结的,我们依旧可以操作:

'use strict';

var person = {
  name: 'Scott',
  info: {
    weight: 65,
    height: 175
  }
};

Object.freeze(person);

console.log(Object.isFrozen(person)); //true

person.info.age = 20; //add 'age' property to info
person.info.weight = 70;  //change the 'weight' property

上面代码中,我们向info中添加age属性,或者更改weight的值,这些操作都是没有问题的,所以如果需要对一个对象及下面的属性进行完全的冻结,我们需要递归的对每一个属性对象进行冻结。


以上就是ES5对Object的增强,有了这些强大的API,可以确保我们的代码更加安全,进而可以构建出健壮的应用。另外,在上面的示例中,我们多次提到了严格模式,这也是ES5规范中重要的一个方面,下次我们会对严格模式进行一个全面的概括。


你可能感兴趣的:(ES5规范之Object增强)