Free email newsletter: “ES.next News

2012-08-08

Properties in JavaScript: definition versus assignment

Did you know that defining a property is not the same as assigning to it? This blog post explains the difference and its consequences. It has been triggered by an email from Allen Wirfs-Brock on the es-discuss mailing list.

Definition versus assignment

Definition. To define a property, one uses a function such as
    Object.defineProperty(obj, propName, propDesc)
The primary purpose of this function is to add an own (direct) property to obj, whose attributes (writable etc., see below) are as specified by propDesc. The secondary purpose is to change the attributes of a property, including its value.

Assignment. To assign to a property, one uses an expression such as

    obj.prop = value
The primary purpose of such an expression is to change the value. Before performing that change, JavaScript consults the prototype chain [1] of obj: If there is a setter somewhere in obj or in one of its prototypes then the assignment is an invocation of that setter. Assignment has the side effect of creating a property if it doesn’t exist, yet – as an own property of obj, with default attributes.

The following two sections go into more detail regarding how definition and assignment work. Feel free to skip them. You should still be able to understand Sect. 4, “The consequences”, and later.

Recap: property attributes and internal properties

Before we can explain how property definition and assignment work, let’s quickly review what property attributes and internal properties are.

Kinds of properties

JavaScript distinguishes three kinds of properties:
  • Named accessor properties: A property that exists thanks to a getter or a setter.
  • Named data properties: A property that h as a value. Those are the most common properties. They include methods.
  • Internal properties: are used internally by JavaScript and not directly accessible via the language. However, there can be indirect ways of accessing them. Example: Every object has an internal property called [[Prototype]]. You cannot directly read it, but still retrieve its value, via Object.getPrototypeOf(). While internal properties are referred to by a name in square brackets, they are nameless in the sense that they are invisible and don’t have a normal, string-valued property name.

Property attributes

Property attributes are fields that every property has and that influence how it works. The following attributes exist:
  • All properties:
    • [[Enumerable]]: If a property is non-enumerable, it can’t be seen by some operations, such as for...in and Object.keys() [2].
    • [[Configurable]]: If a property is non-configurable, none of the attributes (except [[Value]]) can be changed via a definition.
  • Named data properties:
    • [[Value]]: is the value of the property.
    • [[Writable]]: determines whether the value can be changed.
  • Named accessor properties:
    • [[Get]]: holds a getter method.
    • [[Set]]: holds a setter method.

Property descriptors

A property descriptor is a set of property attributes encoded as an object. For example:
    {
        value: 123,
        writable: false
    }
You can see that the property names correspond to the attribute names [[Value]] and [[Writable]]. Property descriptors are used by functions such as Object.defineProperty, Object.getOwnPropertyDescriptor and Object.create that either change or return the attributes of a property. If properties are missing from a descriptor, the following defaults apply:

Property Default value
valueundefined
get undefined
set undefined
writable false
enumerable false
configurable false

Internal properties

There are several internal properties that all objects have. Among others, the following four ones:
  • [[Prototype]]: The prototype of the object.
  • [[Extensible]]: Is this object extensible, can new properties be added to it?
  • [[DefineOwnProperty]]: Define a property. See explanation below.
  • [[Put]]: Assign to a property. See explanation below.

The details of definition and assignment

Defining a property

Defining a property is handled by the internal method
[[DefineOwnProperty]] (P, Desc, Throw)
P is the name of a property. Throw specifies how the operation should reject a change: If Throw is true then an exception is thrown. Otherwise, the operation is silently aborted. When [[DefineOwnProperty]] is called, the following steps are performed.
  • If this does not have an own property whose name is P: Create a new property if the object is extensible, reject if it isn’t.
  • Otherwise, there already is an own property and the definition changes that property.
  • If that property is not configurable then the following changes will be rejected:
    • Converting a data property to an accessor property or vice versa
    • Changing [[Configurable]] or [[Enumerable]]
    • Changing [[Writable]]
    • Changing [[Value]] if [[Writable]] is false
    • Changing [[Get]] or [[Set]]
  • Otherwise, the existing own property is configurable and can be changed as specified.
If Desc exactly mirrors the current attributes of this[P] then the definition is never rejected.

Two functions for defining a property are Object.defineProperty and Object.defineProperties. For example:

    Object.defineProperty(obj, propName, desc)
Internally, that leads to the following method invocation:
    obj.[[DefineOwnProperty]](propName, desc, true)

Assigning to a property

Assigning to a property is handled via the internal method
[[Put]] (P, Value, Throw)
P and Throw work the same as with [[DefineOwnProperty]]. When [[Put]] is called, the following steps are performed.
  • If there is a read-only property whose name is P somewhere in the prototype chain: reject.
  • If there is a setter whose name is P somewhere in the prototype chain: call the setter.
  • If there is no own property whose name is P: if the the object is extensible then create a new property.
        this.[[DefineOwnProperty]](
            P,
            {
                value: Value,
                writable: true,
                enumerable: true,
                configurable: true
            },
            Throw
        )
    
    If the object is not extensible then reject.
  • Otherwise, there is an own property named P that is writable. Invoke
        this.[[DefineOwnProperty]](P, { value: Value }, Throw)
    
    That updates the value of P, but keeps its attributes (such as enumerability) unchanged
The assignment operator (=) calls [[Put]]. For example:
    obj.prop = v;
Internally, that triggers the call
    obj.[[Put]]("prop", v, isStrictModeOn)
That is, the assignment operator only throws if is performed in strict mode. [[Put]] does not return Value, but the assignment operator does.

The consequences

This section describes some consequences of how property definition and assignment work.

Assignment calls a setter in a prototype, definition creates an own property

Given the following empty object obj whose prototype proto has a getter/setter pair called foo.
    var proto = {
        get foo() {
            console.log("Getter");
            return "a";
        },
        set foo(x) {
            console.log("Setter: "+x);
        },
    };
    var obj = Object.create(proto);
What is the difference between defining the property foo of obj versus assigning to it? If you define then your intention is to create a new property. Those are always created in the first object of a prototype chain, which in this case means in obj:
    > Object.defineProperty(obj, "foo", { value: "b" });
    > obj.foo
    'b'
    > proto.foo
    Getter
    'a'
If, instead, you assign to foo then your intention is to change something that already exists and that change should be handled via the setter. And it turns out that an assignment does call the setter:
    > obj.foo = "b";
    Setter: b
    'b'
You can make a property read-only, by only defining a getter. Below, property bar of object proto2 is such a property and inherited by obj2.
    "use strict";
    var proto2 = {
        get bar() {
            console.log("Getter");
            return "a";
        },
    };
    var obj2 = Object.create(proto2);
We use strict mode so that an exception will be thrown if we make an assignment. Otherwise, the assignment would be simply ignored (but not change obj, either). With an assignment, we want to change bar which is forbidden due to bar being read-only.
    > obj2.bar = "b";
    TypeError: obj.bar is read-only
We can, however, define something new and thus override proto’s property bar:
    > Object.defineProperty(obj2, "bar", { value: "b" });
    > obj2.bar
    'b'
    > proto2.bar
    Getter
    'a'

Read-only properties in prototypes prevent assignment, but not definition

A read-only property in a prototype prevents assignment from adding an own property with the same name, you need to use definition if you want to do so. That restriction has been newly introduced in ECMAScript 5.1. That is, the assignment obj.foo = "b" does not auto-create the property foo if there is a read-only property with that name in one of obj’s prototypes. This is demonstrated via the following example, where obj’s prototype proto has a read-only property foo whose value is "a".
    "use strict";
    var proto = Object.defineProperties(
        {},
        {
            foo: {  // attributes of property foo:
                value: "a",
                writable: false,  // read-only
                configurable: true  // explained later
            }
        });
    var obj = Object.create(proto);
Assignment. Assignment results in an exception:
    > obj.foo = "b";
    TypeError: obj.foo is read-only
It is surprising that an inherited property is able to influence whether or not an own property can be created [3]. But it makes sense, because that is also how a getter-only property works.

Definition. With definition, we want to create a new own property:

    > Object.defineProperty(obj, "foo", { value: "b" });
    > obj.foo
    'b'
    > proto.foo
    'a'

The assignment operator does not change properties in prototypes

Given the following setup, where obj inherits the property foo from proto.
    var proto = { foo: "a" };
    var obj = Object.create(proto);
You can’t change proto.foo by assigning to obj.foo. Doing so creates a new own property:
    > obj.foo = "b";
    'b'
    > obj.foo
    'b'
    > proto.foo
    'a'
The rationale for this behavior is as follows: Prototypes can introduce properties whose values are shared by all of their descendants. If one decides to change such a property in a descendant, a new own property is created. That means one can make the change, but it is only local, it doesn’t affect the other descendants. In this light, the effects of getter-only properties and read-only properties make sense: prevent changes, by preventing the creation of an own property. What is the motivation for overriding prototype properties instead of changing them?
  1. Methods: Allow methods to be patched, directly in the prototype, but prevent accidental changes via descendants of the prototype.
  2. Non-method properties: The prototype provides shared default values for descendants. One can override these values via a descendant, but not change them. This is considered an anti-pattern and discouraged. It is cleaner to assign default values in constructors.

Only definition allows you to create a property with arbitrary attributes

If you create an own property via assignment, it always has default attributes. If you want to specify arbitrary attributes, you must use definition. Note that that includes adding getters and setters to an object.

The properties of an object literal are added via definition

Given the following object literal.
    var obj = {
        foo: 123
    };
This is internally translated to a series of statements. You have two options. First, via assignment:
    var obj = new Object();
    obj.foo = 123;
Second, via definition:
    var obj = new Object();
    Object.defineProperties(obj, {
        foo: {
            value: 123,
            enumerable: true,
            configurable: true,
            writable: true
        }
    });
The second option better expresses the semantics of an object literal: To create fresh properties. It is for the same reason that Object.create receives property descriptors via its second argument.

Attributes of methods

One possibility (suggested by Wirfs-Brock) is to give methods the following attributes:
    "use strict";
    function Stack() {
    }
    Object.defineProperties(Stack.prototype, {
        push: {
            writable: false,
            configurable: true,
            value: function (x) { /* ... */ }
        }
    });
The idea is to prevent accidental assignment:
    > var s = new Stack();

    > s.push = 5;
    TypeError: s.push is read-only
However, as push is configurable, we can override it in an instance via property definition.
    > var s = new Stack();
    > Object.defineProperty(s, "push",
          { value: function () { return "yes" }})

    > s.push()
    'yes'
Definition would also allow us to replace Stack.prototype.push.

Conclusion

Property assignment is frequently used to add new properties to an object. This post explained that that can cause problems. Hence, it is best to follow the simple rules:
  1. If you want to create a new property, use definition.
  2. If you want to change the value of a property, use assignment.
In the comments, medikoo reminds us that using property descriptors to achieve #1 is slow. And I indeed often use assignment to create properties, because it’s also more convenient. Thankfully, ECMAScript.next might make definition both faster and more convenient: There is a proposal for a “define properties” operator, an alternative to Object.defineProperties. Given that the difference between definition and assignment is subtle, yet important, such help with definition would be welcome.

References

  1. Prototypes as classes – an introduction to JavaScript inheritance
  2. JavaScript properties: inheritance and enumerability
  3. Fixing the Read-only Override Prohibition Mistake [a page on the ECMAScript wiki with background information on this issue]

No comments: