There are two ways of creating or changing a property prop
of an object obj
:
obj.prop = true
Object.defineProperty(obj, '', {value: true})
This chapter explains how they work.
Required knowledge: property attributes and property descriptors
For this chapter, you should be familiar with property attributes and property descriptors. If you aren’t, check out §9 “Property attributes: an introduction”.
We use the assignment operator =
to assign a value value
to a property .prop
of an object obj
:
This operator works differently depending on what .prop
looks like:
Changing properties: If there is an own data property .prop
, assignment changes its value to value
.
Invoking setters: If there is an own or inherited setter for .prop
, assignment invokes that setter.
Creating properties: If there is no own data property .prop
and no own or inherited setter for it, assignment creates a new own data property.
That is, the main purpose of assignment is making changes. That’s why it supports setters.
To define a property with the key propKey
of an object obj
, we use an operation such as the following method:
This method works differently depending on what the property looks like:
propKey
exists, defining changes its property attributes as specified by the property descriptor propDesc
(if possible).propDesc
(if possible).That is, the main purpose of definition is to create an own property (even if there is an inherited setter, which it ignores) and to change property attributes.
Property descriptors in the ECMAScript specification
In specification operations, property descriptors are not JavaScript objects but Records, a spec-internal data structure that has fields. The keys of fields are written in double brackets. For example, Desc.[[Configurable]]
accesses the field .[[Configurable]]
of Desc
. These records are translated to and from JavaScript objects when interacting with the outside world.
The actual work of assigning to a property is handled via the following operation in the ECMAScript specification:
OrdinarySetWithOwnDescriptor(O, P, V, Receiver, ownDesc)
These are the parameters:
O
is the object that is currently being visited.P
is the key of the property that we are assigning to.V
is the value we are assigning.Receiver
is the object where the assignment started.ownDesc
is the descriptor of O[P]
or null
if that property doesn’t exist.The return value is a boolean that indicates whether or not the operation succeeded. As explained later in this chapter, strict-mode assignment throws a TypeError
if OrdinarySetWithOwnDescriptor()
fails.
This is a high-level summary of the algorithm:
Receiver
until it finds a property whose key is P
. The traversal is done by calling OrdinarySetWithOwnDescriptor()
recursively. During recursion, O
changes and points to the object that is currently being visited, but Receiver
stays the same.Receiver
(where recursion started) or something else happens.In more detail, this algorithm works as follows:
ownDesc
is undefined
, then we haven’t yet found a property with key P
:
If O
has a prototype parent
, then we return parent.[[Set]](P, V, Receiver)
. This continues our search. The method call usually ends up invoking OrdinarySetWithOwnDescriptor()
recursively.
Otherwise, our search for P
has failed and we set ownDesc
as follows:
{
[[Value]]: undefined, [[Writable]]: true,
[[Enumerable]]: true, [[Configurable]]: true
}
With this ownDesc
, the next if
statement will create an own property in Receiver
.
ownDesc
specifies a data property, then we have found a property:
ownDesc.[[Writable]]
is false
, return false
. This means that any non-writable property P
(own or inherited!) prevents assignment.existingDescriptor
be Receiver.[[GetOwnProperty]](P)
. That is, retrieve the descriptor of the property where the assignment started. We now have:
O
and the current property descriptor ownDesc
on one hand.Receiver
and the original property descriptor existingDescriptor
on the other hand.existingDescriptor
is not undefined
:
Receiver
does not have a property P
.)if
conditions should never be true
because ownDesc
and existingDesc
should be equal:
existingDescriptor
specifies an accessor, return false
.existingDescriptor.[[Writable]]
is false
, return false
.Receiver.[[DefineOwnProperty]](P, { [[Value]]: V })
. This internal method performs definition, which we use to change the value of property Receiver[P]
. The definition algorithm is described in the next subsection.Receiver
does not have an own property with key P
.)CreateDataProperty(Receiver, P, V)
. (This operation creates an own data property in its first argument.)ownDesc
describes an accessor property that is own or inherited.)setter
be ownDesc.[[Set]]
.setter
is undefined
, return false
.Call(setter, Receiver, «V»)
. Call()
invokes the function object setter
with this
set to Receiver
and the single parameter V
(French quotes «»
are used for lists in the specification).true
.OrdinarySetWithOwnDescriptor()
?Evaluating an assignment without destructuring involves the following steps:
AssignmentExpression
. This section handles providing names for anonymous functions, destructuring, and more.PutValue()
is used to make the assignment.PutValue()
invokes the internal method .[[Set]]()
..[[Set]]()
calls OrdinarySet()
(which calls OrdinarySetWithOwnDescriptor()
) and returns the result.Notably, PutValue()
throws a TypeError
in strict mode if the result of .[[Set]]()
is false
.
The actual work of defining a property is handled via the following operation in the ECMAScript specification:
ValidateAndApplyPropertyDescriptor(O, P, extensible, Desc, current)
The parameters are:
O
where we want to define a property. There is a special validation-only mode where O
is undefined
. We are ignoring this mode here.P
of the property we want to define.extensible
indicates if O
is extensible.Desc
is a property descriptor specifying the attributes we want the property to have.current
contains the property descriptor of an own property O[P]
if it exists. Otherwise, current
is undefined
.The result of the operation is a boolean that indicates if it succeeded. Failure can have different consequences. Some callers ignore the result. Others, such as Object.defineProperty()
, throw an exception if the result is false
.
This is a summary of the algorithm:
If current
is undefined
, then property P
does not currently exist and must be created.
extensible
is false
, return false
indicating that the property could not be added.Desc
and create either a data property or an accessor property.true
.If Desc
doesn’t have any fields, return true
indicating that the operation succeeded (because no changes had to be made).
If current.[[Configurable]]
is false
:
Desc
is not allowed to change attributes other than value
.)Desc.[[Configurable]]
exists, it must have the same value as current.[[Configurable]]
. If not, return false
.Desc.[[Enumerable]]
Next, we validate the property descriptor Desc
: Can the attributes described by current
be changed to the values specified by Desc
? If not, return false
. If yes, go on.
false
is returned..[[Configurable]]
and .[[Enumerable]]
are preserved, all other attributes get default values (undefined
for object-valued attributes, false
for boolean-valued attributes).current.[[Configurable]]
and current.[[Writable]]
are false
, then no changes are allowed and Desc
and current
must specify the same attributes:
current.[[Configurable]]
being false
, Desc.[[Configurable]]
and Desc.[[Enumerable]]
were already checked previously and have the correct values.)Desc.[[Writable]]
exists and is true
, then return false
.Desc.[[Value]]
exists and does not have the same value as current.[[Value]]
, then return false
.true
indicating that the algorithm succeeded.current.[[Configurable]]
is false
, then no changes are allowed and Desc
and current
must specify the same attributes:
current.[[Configurable]]
being false
, Desc.[[Configurable]]
and Desc.[[Enumerable]]
were already checked previously and have the correct values.)Desc.[[Set]]
exists, it must have the same value as current.[[Set]]
. If not, return false
.Desc.[[Get]]
true
indicating that the algorithm succeeded.Set the attributes of the property with key P
to the values specified by Desc
. Due to validation, we can be sure that all of the changes are allowed.
Return true
.
This section describes some consequences of how property definition and assignment work.
If we create an own property via assignment, it always creates properties whose attributes writable
, enumerable
, and configurable
are all true
.
const obj = {};
obj.dataProp = 'abc';
assert.deepEqual(
Object.getOwnPropertyDescriptor(obj, 'dataProp'),
{
value: 'abc',
writable: true,
enumerable: true,
configurable: true,
});
Therefore, if we want to specify arbitrary attributes, we must use definition.
And while we can create getters and setters inside object literals, we can’t add them later via assignment. Here, too, we need definition.
Let us consider the following setup, where obj
inherits the property prop
from proto
.
We can’t (destructively) change proto.prop
by assigning to obj.prop
. Doing so creates a new own property:
assert.deepEqual(
Object.keys(obj), []);
obj.prop = 'b';
// The assignment worked:
assert.equal(obj.prop, 'b');
// But we created an own property and overrode proto.prop,
// we did not change it:
assert.deepEqual(
Object.keys(obj), ['prop']);
assert.equal(proto.prop, 'a');
The rationale for this behavior is as follows: Prototypes can have properties whose values are shared by all of their descendants. If we want to change such a property in only one descendant, we must do so non-destructively, via overriding. Then the change does not affect the other descendants.
What is the difference between defining the property .prop
of obj
versus assigning to it?
If we define, then our intention is to either create or change an own (non-inherited) property of obj
. Therefore, definition ignores the inherited setter for .prop
in the following example:
let setterWasCalled = false;
const proto = {
get prop() {
return 'protoGetter';
},
set prop(x) {
setterWasCalled = true;
},
};
const obj = Object.create(proto);
assert.equal(obj.prop, 'protoGetter');
// Defining obj.prop:
Object.defineProperty(
obj, 'prop', { value: 'objData' });
assert.equal(setterWasCalled, false);
// We have overridden the getter:
assert.equal(obj.prop, 'objData');
If, instead, we assign to .prop
, then our intention is often to change something that already exists and that change should be handled by the setter:
let setterWasCalled = false;
const proto = {
get prop() {
return 'protoGetter';
},
set prop(x) {
setterWasCalled = true;
},
};
const obj = Object.create(proto);
assert.equal(obj.prop, 'protoGetter');
// Assigning to obj.prop:
obj.prop = 'objData';
assert.equal(setterWasCalled, true);
// The getter still active:
assert.equal(obj.prop, 'protoGetter');
What happens if .prop
is read-only in a prototype?
In any object that inherits the read-only .prop
from proto
, we can’t use assignment to create an own property with the same key – for example:
const obj = Object.create(proto);
assert.throws(
() => obj.prop = 'objValue',
/^TypeError: Cannot assign to read only property 'prop'/);
Why can’t we assign? The rationale is that overriding an inherited property by creating an own property can be seen as non-destructively changing the inherited property. Arguably, if a property is non-writable, we shouldn’t be able to do that.
However, defining .prop
still works and lets us override:
Accessor properties that don’t have a setter are also considered to be read-only:
const proto = {
get prop() {
return 'protoValue';
}
};
const obj = Object.create(proto);
assert.throws(
() => obj.prop = 'objValue',
/^TypeError: Cannot set property prop of #<Object> which has only a getter$/);
The “override mistake”: pros and cons
The fact that read-only properties prevent assignment earlier in the prototype chain, has been given the name override mistake:
In this section, we examine where the language uses definition and where it uses assignment. We detect which operation is used by tracking whether or not inherited setters are called. See §11.3.3 “Assignment calls setters, definition doesn’t” for more information.
When we create properties via an object literal, JavaScript always uses definition (and therefore never calls inherited setters):
let lastSetterArgument;
const proto = {
set prop(x) {
lastSetterArgument = x;
},
};
const obj = {
__proto__: proto,
prop: 'abc',
};
assert.equal(lastSetterArgument, undefined);
=
always uses assignmentThe assignment operator =
always uses assignment to create or change properties.
let lastSetterArgument;
const proto = {
set prop(x) {
lastSetterArgument = x;
},
};
const obj = Object.create(proto);
// Normal assignment:
obj.prop = 'abc';
assert.equal(lastSetterArgument, 'abc');
// Assigning via destructuring:
[obj.prop] = ['def'];
assert.equal(lastSetterArgument, 'def');
Alas, even though public class fields have the same syntax as assignment, they do not use assignment to create properties, they use definition (like properties in object literals):
let lastSetterArgument1;
let lastSetterArgument2;
class A {
set prop1(x) {
lastSetterArgument1 = x;
}
set prop2(x) {
lastSetterArgument2 = x;
}
}
class B extends A {
prop1 = 'one';
constructor() {
super();
this.prop2 = 'two';
}
}
new B();
// The public class field uses definition:
assert.equal(lastSetterArgument1, undefined);
// Inside the constructor, we trigger assignment:
assert.equal(lastSetterArgument2, 'two');
Section “Prototype chains” in “JavaScript for impatient programmers”
Email by Allen Wirfs-Brock to the es-discuss mailing list: “The distinction between assignment and definition […] was not very important when all ES had was data properties and there was no way for ES code to manipulate property attributes.” [That changed with ECMAScript 5.]