定义一个属性和为一个属性赋值是有区别的.本文解释了两者之间的区别以及各自的作用影响.该话题来自于Allen Wirfs-Brock在es-discuss邮件列表中的一封邮件.
定义(Definition).定义属性需要使用相应的函数,比如:
Object.defineProperty(obj, "prop", propDesc)
如果obj没有prop这个自身属性,则该函数的作用是给obj添加一个自身属性prop并赋值,参数propDesc指定了该属性拥有的特性(可写性,可枚举性等).如果obj已经有了prop这个自身属性,则该函数的作用是修改这个已有属性的特性,当然也包括它的属性值.
赋值(assignment).为一个属性赋值需要使用下面的表达式:
obj.prop = value
如果obj已经有了prop这个自身属性,则该表达式的作用就是修改这个prop属性的值,反之,如果obj没有prop这个自身属性,则该表达式的操作结果就不一定了:首先会查找对象obj的原型链[1],如果原型链中的所有对象都没有名为prop的属性,则结果是在obj身上新建一个自身属性prop,新创建的属性拥有默认的属性特性,且把指定的value赋值给该属性.但如果obj的某个上层原型中上有一个名为prop的属性,那接下来的操作就复杂了,具体请看下面的3.2小节.
JavaScript中有三种类型的属性:
每个属性(property)都拥有4个特性(attribute).两种类型的属性一种有6种属性特性:
属性描述符(property descriptor)可以将一个属性的所有特性编码成一个对象并返回.例如:
{
value: 123,
writable: false}
属性描述符使用在下面的这些函数中:Object.defineProperty, Object.getOwnPropertyDescriptor, Object.create.如果省略了属性描述符对象中的某个属性,则该属性会取一个默认值:
属性名 | 默认值 |
value | undefined |
get | undefined |
set | undefined |
writable | false |
enumerable | false |
configurable | false |
下面列举几个所有对象都有的内部属性:
定义属性是通过内部方法来进行操作的:
[[DefineOwnProperty]] (P, Desc, Throw)
P是要定义的属性名称.参数Throw决定了在定义操作被拒绝的时候是否要抛出异常:如果Throw为true,则抛出异常.否则,操作只会静默失败.当调用[[DefineOwnProperty]]时,具体会执行下面的操作步骤.
如果Desc就是P属性当前的属性描述符,则该定义操作永远不会被拒绝.
定义属性的函数有两个:Object.defineProperty和Object.defineProperties.例如:
Object.defineProperty(obj, propName, desc)
在引擎内部,会转换成这样的方法调用:
obj.[[DefineOwnProperty]](propName, desc, true)
为属性赋值是通过内部方法进行操作的:
[[Put]] (P, Value, Throw)
参数P以及Throw和[[DefineOwnProperty]]方法中的参数表现的一样.在调用[[Put]]方法的时候,会执行下面这样的操作步骤.
this.[[DefineOwnProperty]](
P,
{
value: Value,
writable: true,
enumerable: true,
configurable: true }, Throw)
this.[[DefineOwnProperty]](P, { value: Value }, Throw)
赋值运算符(=)就是在调用[[Put]].比如:
obj.prop = v;
在引擎内部,会转换成这样的方法调用:
obj.[[Put]]("prop", v, isStrictModeOn)
isStrictModeOn对应着参数Throw.也就是说,赋值运算符只有在严格模式下才有可能抛出异常.[[Put]]没有返回值,但赋值运算符有.
本节讲一下属性的定义操作和赋值操作各自的作用及影响.
给定一个空对象obj,他的原型proto有一个名为foo的访问器属性.
var proto = {
get foo() {
console.log("Getter");
return "a";
},
set foo(x) {
console.log("Setter: "+x);
},
};
var obj = Object.create(proto);
那么,"在obj身上定义一个foo属性"和"为obj的foo属性赋值"有什么区别呢?
如果是定义操作的话,则会在obj身上添加一个自身属性foo:
> Object.defineProperty(obj, "foo", { value: "b" });
> obj.foo
'b'
> proto.foo
Getter
'a'
但如果为foo属性赋值的话,则意味着你是想改变某个已经存在的属性的值.所以这次赋值操作会转交给原型proto的foo属性的setter访问器来处理,下面代码的执行结果就能证明这一结论:
> obj.foo = "b";
Setter: b
'b'
你还可以定义一个只读的访问器属性,办法是:只定义一个getter,省略setter.下面的例子中,proto2的bar属性就是这样的只读属性,obj2继承了这个属性.
"use strict";
var proto2 = {
get bar() {
console.log("Getter");
return "a";
},
};
var obj2 = Object.create(proto2);
开启严格模式的话,下面的赋值操作会抛出异常.非严格模式的话,赋值操作只会静默失败(不起任何作用,也不报错).
> obj2.bar = "b";
TypeError: obj.bar is read-only
我们可以定义一个自身属性bar,遮蔽从原型上继承的bar属性:
> Object.defineProperty(obj2, "bar", { value: "b" });
> obj2.bar
'b'
> proto2.bar
Getter
'a'
如果原型链中存在一个同名的只读属性,则无法通过赋值的方式在原对象上添加这个自身属性,必须使用定义操作才可以.这项限制是在ECMAScript 5.1中引入的:
"use strict";
var proto = Object.defineProperties(
{},
{
foo: { // foo属性的特性: value: "a",
writable: false, // 只读 configurable: true // 可配置 }
});
var obj = Object.create(proto);
赋值.赋值操作会导致异常:
> obj.foo = "b"; //译者注:貌似只有Firefox遵循了这个标准TypeError: obj.foo is read-only
这貌似是个很奇怪的表现,原型上的属性居然可以影响到能否创建一个同名的自身属性 [3].但是这样的表现是有道理的,因为另外一种形式的只读属性(只有getter的访问器属性)也是这样的表现,这样才能统一.
定义.通过定义的方式,我们可以成功创建一个新的自身属性:
> Object.defineProperty(obj, "foo", { value: "b" });
> obj.foo
'b'
> proto.foo
'a'
执行下面的代码,则obj会从proto上继承到foo属性.
var proto = { foo: "a" };
var obj = Object.create(proto);
你不能通过为obj.foo赋值来改变proto.foo的值.这种操作只会在obj上新建一个自身属性:
> obj.foo = "b";
'b'
> obj.foo
'b'
> proto.foo
'a'
如果通过赋值操作创建了一个自身属性,则该属性始终拥有默认的特性.如果你想指定某个特性的值,必须通过定义操作.
下面的代码将变量obj指向了一个对象字面量:
var obj = {
foo: 123};
这样的语句在引擎内部,可能会被转换成下面两种操作方式中的一种.首先可能是赋值操作:
var obj = new Object();
obj.foo = 123;
其次,可能是个定义操作:
var obj = new Object();
Object.defineProperties(obj, {
foo: {
value: 123,
enumerable: true,
configurable: true,
writable: true }});
到底是哪种呢?正确答案是第二种,因为第二种操作方式能够更好的表达出对象字面量的语义:创建新的属性.Object.create接受一个属性描述符作为第二个可选参数,也是这个原因.
可以通过定义操作新建一个只读的方法属性:
"use strict";
function Stack() {
}
Object.defineProperties(Stack.prototype, {
push: {
writable: false,
configurable: true,
value: function (x) { /* ... */ }
}
});
目的是为了防止在实例身上发生意外的赋值操作:
> var s = new Stack();
> s.push = 5;
TypeError: s.push is read-only
不过,由于push是可配置的,所以我们仍可以通过定义操作来为实例添加一个自身的push方法.
> var s = new Stack();
> Object.defineProperty(s, "push",
{ value: function () { return "yes" }})
> s.push()
'yes'
我们甚至可以通过定义操作来重新定义原型上的push方法:Stack.prototype.push.
我们经常会通过为属性赋值来给一个对象添加新的属性.本文解释了这样做是可能引发一些问题的.因此,最好要遵循下面的规则:
在评论中,medikoo提醒了我们使用属性描述符来创建属性可能会有点慢.我就经常通过为属性赋值来创建新属性,因为这样很方便.值得高兴的是,ECMAScript.next也许会把属性的定义操作变的既快又方便:已经存在一个“定义属性的运算符”的提案,可以作为Object.defineProperties的替代用法.由于属性定义和属性赋值之间的差别既很微妙,又很重要,所以这种改进应该会很受欢迎.
英文原文:http://www.2ality.com/2012/08/property-definition-assignment.html
中文原文:http://www.cnblogs.com/ziyunfei/archive/2012/10/31/2738728.html