[译]JavaScript中的属性:定义和赋值的区别

来源:互联网 发布:论坛发帖推广软件 编辑:程序博客网 时间:2024/04/30 03:29
[译]JavaScript中的属性:定义和赋值的区别

原文:http://www.2ality.com/2012/08/property-definition-assignment.html

你知道吗?定义一个属性和为一个属性赋值是有区别的.本文解释了两者之间的区别以及各自的作用影响.该话题来自于Allen Wirfs-Brock在es-discuss邮件列表中的一封邮件.

1. 定义VS赋值

定义(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小节.

2. 属性特性和内部属性

2.1. 多种类型的属性

JavaScript中有三种类型的属性:

  • 命名数据属性(named data properties): 拥有一个确定的值的属性.这也是最常用的属性.
  • 命名访问器属性(named accessor properties): 通过getter和setter进行读取和赋值的属性.
  • 内部属性(internal properties): 由JavaScript引擎内部使用的属性,不能通过JavaScript代码直接访问到,不过可以通过一些方法间接的读取和设置.比如:每个对象都有一个内部属性[[Prototype]],你不能直接访问这个属性,但可以通过Object.getPrototypeOf()方法间接的读取到它的值.虽然内部属性通常用一个双中括号包围的名称来表示,但实际上这并不是它们的名字,它们是一种抽象操作,是不可见的,根本没有上面两种属性有的那种字符串类型的属性名.

2.2. 属性特性

每个属性(property)都拥有4个特性(attribute).两种类型的属性一种有6种属性特性:

  • 命名数据属性特有的特性:
    • [[Value]]: 属性的值.
    • [[Writable]]: 控制属性的值是否可以改变.
  • 命名访问器属性特有的特性:
    • [[Get]]: 存储着getter方法.
    • [[Set]]: 存储着setter方法.
  • 两种属性都有的特性:
    • [[Enumerable]]: 如果一个属性是不可枚举的,则在一些操作下,这个属性是不可见的,比如for...in和Object.keys()[2].
    • [[Configurable]]: 如果一个属性是不可配置的,则该属性的所有特性(除了[[Value]])都不可改变.

2.3. 属性描述符

属性描述符(property descriptor)可以将一个属性的所有特性编码成一个对象并返回.例如:

{    value: 123,    writable: false}

属性描述符使用在下面的这些函数中:Object.definePropertyObject.getOwnPropertyDescriptorObject.create.如果省略了属性描述符对象中的某个属性,则该属性会取一个默认值:

属性名默认值valueundefinedgetundefinedsetundefinedwritablefalseenumerablefalseconfigurablefalse

2.4. 内部属性

下面列举几个所有对象都有的内部属性:

  • [[Prototype]]: 对象的原型.
  • [[Extensible]]: 对象是否可以扩展,也就是是否可以添加新的属性.
  • [[DefineOwnProperty]]: 定义一个属性的内部方法.下一节会详细解释.
  • [[Put]]: 为一个属性赋值的内部方法.下一节会详细解释.

3. 属性定义和属性赋值

3.1. 属性定义

定义属性是通过内部方法来进行操作的:

[[ DefineOwnProperty]] (P, Desc, Throw)

P是要定义的属性名称.参数Throw决定了在定义操作被拒绝的时候是否要抛出异常:如果Throw为true,则抛出异常.否则,操作只会静默失败.当调用[[DefineOwnProperty]]时,具体会执行下面的操作步骤.

  • 如果this没有名为P的自身属性的话:如果this是可扩展的话,则创建P这个自身属性,否则拒绝.
  • 如果this已经有了名为P的自身属性:则按照下面的步骤重新配置这个属性.
  • 如果这个已有的属性是不可配置的,则进行下面的操作会被拒绝:
    • 将一个数据属性转换成访问器属性,反之亦然
    • 改变[[Configurable]]或[[Enumerable]]
    • 该变[[Writable]]
    • 在[[Writable]]为false时改变[[Value]]
    • 改变[[Get]]或[[Set]]
  • 否则,这个已有的属性可以被重新配置.

如果Desc就是P属性当前的属性描述符,则该定义操作永远不会被拒绝.

定义属性的函数有两个:Object.defineProperty和Object.defineProperties.例如:

Object.defineProperty(obj, propName, desc)

在引擎内部,会转换成这样的方法调用:

obj.[[DefineOwnProperty]](propName, desc, true)

3.2. 属性赋值

为属性赋值是通过内部方法进行操作的:

[[ Put]] (P, Value, Throw)

参数P以及Throw和[[DefineOwnProperty]]方法中的参数表现的一样.在调用[[Put]]方法的时候,会执行下面这样的操作步骤.

  • 如果在原型链上存在一个名为P的只读属性(只读的数据属性或者没有setter的访问器属性),则拒绝.
  • 如果在原型链上存在一个名为P的且拥有setter的访问器属性:则调用这个setter.
  • 如果没有名为P的自身属性:则如果这个对象是可扩展的,就使用下面的操作创建一个新属性:
    this.[[DefineOwnProperty]](    P,    {        value: Value,        writable: true,        enumerable: true,        configurable: true    },    Throw)
    否则,如果这个对象是不可扩展的,则拒绝.
  • 否则, 如果已经存在一个可写的名为P的自身属性.则调用:
    this.[[DefineOwnProperty]](P, { value: Value }, Throw)
    该操作只会更改P属性的值,其他的特性(比如可枚举性)都不会改变.

赋值运算符(=)就是在调用[[Put]].比如:

obj.prop = v;

在引擎内部,会转换成这样的方法调用:

obj.[[Put]]("prop", v, isStrictModeOn)

isStrictModeOn对应着参数Throw.也就是说,赋值运算符只有在严格模式下才有可能抛出异常.[[Put]]没有返回值,但赋值运算符有.

4. 作用及影响

本节讲一下属性的定义操作和赋值操作各自的作用及影响.

4.1. 赋值可能会调用原型上的setter,定义会创建一个自身属性

给定一个空对象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.fooGetter '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.barGetter 'a'

4.2. 原型链中的同名只读属性可能会阻止赋值操作,但不会阻止定义操作

如果原型链中存在一个同名的只读属性,则无法通过赋值的方式在原对象上添加这个自身属性,必须使用定义操作才可以.这项限制是在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'

4.3.  赋值运算符不会改变原型链上的属性

执行下面的代码,则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'

4.4. 只有通过定义操作,才能创建一个拥有指定特性的属性

如果通过赋值操作创建了一个自身属性,则该属性始终拥有默认的特性.如果你想指定某个特性的值,必须通过定义操作.

4.5. 对象字面量中的属性是通过定义操作添加的

下面的代码将变量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接受一个属性描述符作为第二个可选参数,也是这个原因.

4.6. 方法属性

可以通过定义操作新建一个只读的方法属性:

"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.

5. 结论

我们经常会通过为属性赋值来给一个对象添加新的属性.本文解释了这样做是可能引发一些问题的.因此,最好要遵循下面的规则:

  1. 如果你想创建一个新属性,则使用属性定义.
  2. 如果你想该变一个属性的值,则使用属性赋值.

在评论中,medikoo提醒了我们使用属性描述符来创建属性可能会有点慢.我就经常通过为属性赋值来创建新属性,因为这样很方便.值得高兴的是,ECMAScript.next也许会把属性的定义操作变的既快又方便:已经存在一个“定义属性的运算符”的提案,可以作为Object.defineProperties的替代用法.由于属性定义和属性赋值之间的差别既很微妙,又很重要,所以这种改进应该会很受欢迎.

0 0
原创粉丝点击