ECMAScript Object.defineProperty 属性描述符

ECMAScript 5 出了一个【属性描述符】,主要是为了【给属性增加更多的控制】。下面我们就谈论这样一个新的特性-Object.defineProperty()

Object.defineProperty() 方法直接在一个对象上定义一个新属性,或者修改一个已经存在的属性, 并返回这个对象


语法定义

Object.defineProperty(obj, prop, descriptor)

  • obj : 需要定义属性的对象
  • prop : 需被定义或修改的属性名
  • descriptor : 需被定义或修改的属性的描述符

为什么我们要用这个新特性?

我们给对象创建属性有下面这样几种方法:

1
2
3
4
5
6
7
8
9
10
var person = {};
//第一种方式
person.name = 'Seven';
//第二种方式
person[name] = 'Seven';
//第三种方式
Object.defineProperty(person, 'name', {
value : 'Seven'
})
person.name //Seven

看起来好像最后一种新方法最麻烦,那我们为什么还要用最后一种方法呢?因为后面的descriptor可以让我们制定更多对对象属性的策略。


descriptor数据描述符-writable

上面我们提到了更多策略,其实这个更多策略就是让属性可以更多不同的权限,比如让它只能读,不能写。

当属性特性(property attribute) writable 设置为false时,表示 non-writable,属性不能被修改。

1
2
3
4
5
6
7
8
Object.defineProperty(person,'sex'{
value:'girl',
writable:false
})

person.sex // 'girl'
person.sex = 'boy'
person.sex //girl

当我们设置了descriptor里面的writable为false的时候,这个sex就不可以更改了。虽然你可以设置,但是设置后结果还是girl不会变的。

有人会说,这样不是很不好吗?如果我们在不知情的情况下改写了,连报错都不报。会造成很多bug。很难排查出到底是哪里出了bug。

解决方法是写上use strict。这样就会报错了。

1
2
3
4
5
6
7
8
// 定义严格模式
'use strict';
var obj = { };
Object.defineProperty(obj, 'attr', {
value: 1,
writable: false
});
obj.attr = 2; // throw exception

descriptor数据描述符-enumerable

上面已经提到了一个writable,表示是否可写。现在在看一个数据描述符enumerable

属性特性 enumerable 定义了对象的属性是否可以在 for…in 循环和 Object.keys() 中被枚举。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
var o = {};
Object.defineProperty(o, "a", {
value : 1,
enumerable:true
});
Object.defineProperty(o, "b", {
value : 2, enumerable:false
});

// enumerable defaults to false
Object.defineProperty(o, "c", {
value : 3
});

// 如果使用直接赋值的方式创建对象的属性,则这个属性的enumerable为true
o.d = 4;

for (var i in o) {
console.log(i);
}
// 打印 'a' 和 'd' (in undefined order)

Object.keys(o); // ["a", "d"]

o.propertyIsEnumerable('a'); // true
o.propertyIsEnumerable('b'); // false
o.propertyIsEnumerable('c'); // false

相当于定义了enumerable后,enumerable可以将其“藏”起来,不被循环看见。

那么属性描述符可以修改吗?比如原来我设置了writable为false,我现在想改变了。那我可以更改吗?这就又有一个专门的属性描述符。


descriptor数据描述符-configurable

如果属性已经存在,Object.defineProperty()将尝试根据描述符中的值以及对象当前的配置来修改这个属性。如果描述符的 configurable 特性为false(即该特性为non-configurable),那么除了 writable 外,其他特性都不能被修改,并且数据和存取描述符也不能相互切换。

如果一个属性的 configurable 为 false,则其 writable 特性也只能修改为 false。

如果尝试修改 non-configurable 属性特性(除 writable 以外),将会产生一个TypeError 异常,除非当前值与修改值相同。

configurable 特性表示对象的属性是否可以被删除,以及除 writable 特性外的其他特性是否可以被修改。

1
2
3
4
5
6
7
8
9
10
var obj = { };
Object.defineProperty(obj, 'attr', {
value: 1,
writable: false,
configurable: true
});
Object.defineProperty(obj, 'attr', {
writable: true
});
obj.attr = 2;

因为configurable是true,所以writable可以更改。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
var o = {};
Object.defineProperty(o, "a", {
get : function(){return 1;},
configurable : false
} );

//因为上面已经对a属性定义了configurable为false,所以下面的都是错误的

// throws a TypeError
Object.defineProperty(o, "a", {
configurable : true
});

// throws a TypeError
Object.defineProperty(o, "a", {
enumerable : true
});

// throws a TypeError (set was undefined previously)
Object.defineProperty(o, "a", {
set : function(){}
});

// throws a TypeError (even though the new get does exactly the same thing)
Object.defineProperty(o, "a", {
get : function(){return 1;}
});

// throws a TypeError
Object.defineProperty(o, "a", {
value : 12
});

console.log(o.a); // logs 1
delete o.a; // Nothing happens
console.log(o.a); // logs 1

存取描述符

上面介绍的是数据描述符,现在介绍下存取描述符。

1
2
3
4
5
6
7
var obj = { };
Object.defineProperty(obj, 'attr', {
set: function(val) { this._attr = Math.max(0, val); },
get: function() { return this._attr; }
});
obj.attr = -1;
console.log(obj.attr); // 0

就是说我们可以利用get/set来对属性进行一定的控制和更改。我们最后通过obj.attr得到的值其实就是通过get返回的值。而我们通过obj.attr = value,设置的值其实就是set的值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
var pattern = {
get: function () {
return 'I alway return this string,whatever you have assigned';
},
set: function () {
this.myname = 'this is my name string';
}
};


function TestDefineSetAndGet() {
Object.defineProperty(this, 'myproperty', pattern);
}


var instance = new TestDefineSetAndGet();
instance.myproperty = 'test';

// 'I alway return this string,whatever you have assigned'
console.log(instance.myproperty);
// 'this is my name string'
console.log(instance.myname);

上面一个例子是从mdn上面学习到的,仔细看,如果能看明白,应该就能明白这个set/get的用法了。


获取对象描述符

Object.getOwnPropertyDescriptor 可以帮助我们获得对象描述符。比如我们定义:

1
2
3
4
5
6
7
8
9
10
11
var obj = {};

Object.defineProperty(obj, attr, {
value : 'a',
writable : false,
configurable : true
})

var descriptor = Object.getOwnPropertyDescriptor(obj, attr);

console.dir(descriptor);

结果是:
{ value: 'a', writable: false, enumerable: false, configurable: true }。这是console.log 的结果。console.dir 会显示的很好看。


注意事项

数据描述符和存取描述符不能混合使用,否则会报错:TypeError: property descriptors must not specify a value or be writable when a getter or setter has been specified

1
2
3
4
5
6
Object.defineProperty(o, "conflict", {
value: 'a',
get: function() {
return 'b';
}
});

定义多个属性

1
2
3
4
5
6
7
8
9
10
11
12
var obj = {};
Object.defineProperties(obj, {
"property1": {
value: true,
writable: true
},
"property2": {
value: "Hello",
writable: false
}
// etc. etc.
});

用途

这一部分是我觉得最有价值的一部分了。是我在这篇文章里看到并学习的。感谢。可以优化对象获取和修改属性的方式。下面我就直接引用这个文章里的描述了。

这个优化对象获取和修改属性方式,是什么意思呢? 过去我们在设置dom节点transform时是这样的。

1
2
3
4
5
//加入有一个目标节点, 我们想设置其位移时是这样的
var targetDom = document.getElementById('target');
var transformText = 'translateX(' + 10 + 'px)';
targetDom.style.webkitTransform = transformText;
targetDom.style.transform = transformText;

通过上面,可以看到如果页面是需要许多动画时,我们这样编写transform属性是十分蛋疼的。

但如果通过Object.defineProperty, 我们则可以

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//这里只是简单设置下translateX的属性,其他如scale等属性可自己去尝试
Object.defineProperty(dom, 'translateX', {
set: function(value) {
var transformText = 'translateX(' + value + 'px)';
dom.style.webkitTransform = transformText;
dom.style.transform = transformText;
}
}

//这样再后面调用的时候, 十分简单
dom.translateX = 10;
dom.translateX = -10;
//甚至可以拓展设置如scale, originX, translateZ,等各个属性,达到下面的效果
dom.scale = 1.5; //放大1.5倍
dom.originX = 5; //设置中心点X


总结

主要就是学习了Object.defineProperty这个新的特性。先前知道这个,但是没有仔细去学习,最近在了解vue的时候,忽然感觉这个要好好学下了。因为这些MVVM大都是通过这个新特性来实现数据绑定的。当然angular不是的,angular是通过脏数据。下面推荐和感谢几篇文章:

  1. MDN-defineProperty
  2. es5-property-descriptors
  3. IMWEB-不会Object.defineProperty你就out了