• [译]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.defineProperty, Object.getOwnPropertyDescriptor, Object.create.如果省略了属性描述符对象中的某个属性,则该属性会取一个默认值:

    属性名 默认值
    value undefined
    get undefined
    set undefined
    writable false
    enumerable false
    configurable false

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

    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'

    之所以会这样设计,是因为:原型身上的属性可以被继承该原型的所有后代所共享.如果想改变其中一个后代的某个继承属性,则会创建一个属于这个后代的自身属性.这就意味着:你可以做一些修改,但仅仅是某一个对象上的修改,不会影响其他的后代.从这个角度看,只读属性(只读的数据属性和只有getter的访问器属性)的这种表现就能说的通了: 阻止修改,通过阻止创建自身属性. 重写原型属性而不是修改它们的动机是什么呢?

    1. 方法: 允许直接在原型身上修改原型的方法,但防止通过原型的后代上的意外操作而修改原型上的方法.
    2. 非方法的属性: 原型可以提供给它的后代一些默认存在的属性.通过操作某个后代,你只能遮蔽这些继承的属性,但无法真正的修改它们.这被认为是一种反面模式(anti-pattern),是不推荐的做法.更好的做法是把默认值赋给构造函数.

    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的替代用法.由于属性定义和属性赋值之间的差别既很微妙,又很重要,所以这种改进应该会很受欢迎.

    6. 参考

    1. Prototypes as classes – an introduction to JavaScript inheritance
    2. JavaScript properties: inheritance and enumerability
    3. Fixing the Read-only Override Prohibition Mistake [ECMAScript wiki]
  • 相关阅读:
    机器学习之--数据构造,函数图显示
    python 用xlwt包把数据导出到excel表中
    python实现Hbase
    Hbase命令
    scrapy Formrequest用法(豆瓣登录案例)
    scrapy meta不用pipe用命令-o
    scrapy之Crawspider 腾讯招聘实战案例
    scrapy选择器归纳
    scrapy response.xpath可以提取包含字符XX的标签
    初涉.....编码__列表__字典
  • 原文地址:https://www.cnblogs.com/ziyunfei/p/2738728.html
Copyright © 2020-2023  润新知