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 = valueThe 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.
{ 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 |
value | undefined |
get | undefined |
set | undefined |
writable | false |
enumerable | false |
configurable | false |
[[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.
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)
[[Put]] (P, Value, Throw)P and Throw work the same as with [[DefineOwnProperty]]. When [[Put]] is called, the following steps are performed.
this.[[DefineOwnProperty]]( P, { value: Value, writable: true, enumerable: true, configurable: true }, Throw )If the object is not extensible then reject.
this.[[DefineOwnProperty]](P, { value: Value }, Throw)That updates the value of P, but keeps its attributes (such as enumerability) unchanged
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.
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-onlyWe 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'
"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-onlyIt 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'
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?
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.
"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-onlyHowever, 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.