关于JavaScript中的属性特性

从es5开始,js中开始拥有了一种描述属性特征的特性(即属性描述符)。根据特性的不同,可以把属性分成两种类型:数据属性和访问器属性。

先说说有哪些特性

为了方便后面的理解,有必要先说一下,有哪几个特性。

  • [[Configurable]] // true or false
  • [[Writable]] // true or false
  • [[Enumerable]] // true or false
  • [[Value]] // everty thing
  • [[set]] // function or undefined
  • [[get]] // function or undefined

纳尼,这些是什么鬼,不懂。
先不要急,现在只需要知道有这几个特性,具体是什么意思,看到后面自然就懂了。

谈define

数据属性和访问器属性都拥有特性,要想修改属性的特性,必须通过两个Object方法,即Object.defineProperty和Object.defineProperties。正如其字面意思,这两个方法都是用来定义(修改)属性的,前者一次只能定义一个属性,后者则可以多个。 不多说了,看API和例子

  • defineProperty(obj, prop, descriptor)
    obj: 将要被添加属性或修改属性的对象
    prop: 对象的属性
    descriptor: 对象属性的特性
    1
    2
    3
    4
    5
    var person = {};
    Object.defineProperty(person, birth, {
    writable: false,
    value: 1995
    }); // 定义了一个不可写,值为1995的新属性

注:在使用defineProperty方法定义新属性时(非修改旧属性),如果不指定,configurable, enumerable和writable特性的默认值都是false。所以,上面代码实际等同于:

1
2
3
4
5
6
7
var person = {};
Object.defineProperty(person, birth, {
configurable: false,
enumerable: false,
writable: false,
value: 1995
});

  • defineProperties(obj, props)
    obj: 将要被添加属性或修改属性的对象
    props: 该对象的一个或多个键值对定义了将要为对象添加或修改的属性的具体配置
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    var person = {};
    Object.defineProperties(person, {
    birth: {
    value: 1995
    },
    age: {
    value: 21,
    writable: true
    }
    }); // 基本同Object.defineProperty,一单一复

数据属性

数据属性包含一个数值的位置,在这个位置可以读取和写入值。数据属性拥有4个特性

  • [[Configurable]]: 表示能否通过delete删除属性,能否修改属性的特性,能否把属性改为访问器属性。默认值: true
  • [[Enumerable]]: 表示能否通过for-in,Object.keys()迭代。默认值:true
  • [[Writable]]: 表示能否修改属性的值。默认值: true
  • [[Value]]: 表示属性的数据值。默认值: undefined

访问器属性

访问器属性不包含数据值,它们包含一对getter和setter函数。
在读取访问器属性时,会调用getter函数(即.操作符);在写入访问器属性时,会调用setter函数并传入新值(即=操作);访问器属性不能直接定义,需要使用后面提到的Object.defineProperty函数定义。访问器属性也拥有4个特性

  • [[Configurable]]: 同数据属性
  • [[Enumerable]]: 同数据属性
  • [[Get]]: 在读取属性时调用的函数。默认值: undefined
  • [[Set]]: 在写入属性时调用的函数。默认值: undefined

动起手来,定义一个访问器属性:

1
2
3
4
5
6
7
8
9
10
11
12
var person = {
birth: 1995,
age: 21
};
Object.defineProperty(person, 'year', {
get: function () {
return this.birth + this.age;
},
set: function (newValue) {
this.age = newValue - this.birth;
}
});

注:getter和setter都是可选的,在非严格模式下,只指定了getter却进行了写入操作,写入的值会被忽略;只指定了setter却进行了读取操作,读取到的属性值为undefined。在严格模式下,则都会报错。

其它一些注意点

  1. 数据特性和访问器特性不能同时存在于一个属性之中,否则会报错
  2. 当configurable为false时,再调用Object.defineProperty方法修改处writable之外的特性,都会导致错误(红宝书点到这里为止)。并且writable只能修改为false,否则也会报错。当writable为true时,即使configurable为false,value也可以修改。
  3. 特性选项不一定是自身选项,如果是继承来的也要考虑,为了确认保留这些默认值,你可能要在这之前冻结 Object.prototype,明确指定所有的选项,或者将proto属性指向null。(纳尼,什么意思,没听懂,看段代码,你就懂了)
    1
    2
    3
    4
    5
    6
    Object.prototype.get = function () {};
    var person = {};
    Object.defineProperty(o, 'name', {
    value: 'beilunyang'
    });
    // TypeError: property descriptors must not specify a value or be writable when a getter or setter has been specified

为什么会报如上这个错误呢?
原因就是第三个参数特性选项本身就是一个对象,执行defineProperty的时候,在判断特性选项对象中某个属性是否存在时使用的内部方法是[[HasProperty]].[[HasProperty]]会在[[Prototype]]上寻找属性,自然而然地找到了Object.prototype上定义的get方法,而数据特性和访问器特性不能同时存在于一个属性之中(get和value),所以产生了如上错误。
解决方法:

1
2
3
4
5
6
7
8
9
10
var des = Object.create(null); 
des.value = 'beilunyang';
Object.defineProperty(person, 'name', des);

// or

Object.defineProperty(person, 'name', {
__proto__: null, //这个操作是很慢的噢,不推荐使用
value: 'beilunyang'
});

参考资料