Skip to content

Exposed Object.defineProperty()

Original algorithm

The algorithm is specified in E5 Section 15.2.3.6:

  1. If Type(O) is not Object throw a TypeError exception.
  2. Let name be ToString(P). (Note: this may have side effects.)
  3. Let desc be the result of calling ToPropertyDescriptor with Attributes as the argument.
  4. Call the [[DefineOwnProperty]] internal method of O with arguments name, desc, and true. (Note: the last argument, true, is the Throw flag.)
  5. Return O.

The algorithm returns the object, which allows chaining; for instance:

var o = {};
Object.defineProperty(o, 'foo',
  { value: 'bar' }
).seal();

ToPropertyDescriptor() is a helper called only from Object.defineProperty() and Object.defineProperties(). It converts a property descriptor expressed as an ECMAScript object into a "specification descriptor", doing boolean coercions and cross checking the descriptor. For instance, ToPropertyDescriptor() will reject any property descriptor which contains fields indicating it is both a data property descriptor and an accessor property descriptor. Example from Node / V8:

> var o = {};
> Object.defineProperty(o, 'foo',
...   { value: 'bar', set: function() {} });
TypeError: Invalid property.  A property cannot both
have accessors and be writable or have a value, #<Object>

The result of the property descriptor conversion is an "internal descriptor". Note that unlike when dealing with existing object properties, this descriptor may not be fully populated, i.e. may be missing fields. From an implementation perspective this means that the descriptor needs to be represented differently. The current implementation doesn't have an explicit representation for the "internal descriptor" which exists for the duration of Object.defineProperty(); the descriptor is represented by a bunch of local variables indicating the presence and coerced values of the descriptor fields (for instance: has_writable and is_writable are separate variables).

The ToPropertyDescriptor() algorithm is reformulated in the restatements section.

Other notes:

  • The key is coerced to a string leniently while the object is just checked and never coerced.
  • [[DefineOwnProperty]] is always called with Throw set to true, so the implementation doesn't need to expose a "throw flag".

First draft

Starting from the original algorithm and inlining both ToPropertyDescriptor() and the [[DefineOwnProperty]] algorithm with exotic behaviors, we get:

  1. If Type(O) is not Object throw a TypeError exception.
  2. Let name be ToString(P). (Note: this may have side effects.)
  3. If Type(O) is not Object throw a TypeError exception.
  4. Let desc be a new, empty Property Descriptor.
  5. If O.[[HasProperty]]("enumerable") === true, then set desc.[[Enumerable]] to ToBoolean(O.[[Get]]("enumerable")).
  6. If O.[[HasProperty]]("configurable") === true, then set desc.[[Configurable]] to ToBoolean(O.[[Get]]("configurable")).
  7. If O.[[HasProperty]]("value") === true, then set desc.[[Value]] to O.[[Get]]("value").
  8. If O.[[HasProperty]]("writable") === true, then set desc.[[Writable]] to ToBoolean(O.[[Get]]("writable")).
  9. If O.[[HasProperty]]("get") === true, then: a. Set desc.[[Get]] to O.[[Get]]("get"). b. If desc.[[Get]] !== undefined and IsCallable(desc.[[Get]]) === false, then throw a TypeError exception.
  10. If O.[[HasProperty]]("set") === true, then: a. Set desc.[[Set]] to O.[[Get]]("set"). b. If desc.[[Set]] !== undefined and IsCallable(desc.[[Set]]) === false, then throw a TypeError exception.
  11. If either desc.[[Get]] or desc.[[Set]] are present, then: a. If either desc.[[Value]] or desc.[[Writable]] are present, then throw a TypeError exception.
  12. Let Throw be true.
  13. Set pendingWriteProtect to false.
  14. If O is not an Array object, goto SKIPARRAY.
  15. Let oldLenDesc be the result of calling the [[GetOwnProperty]] internal method of O passing "length" as the argument. The result will never be undefined or an accessor descriptor because Array objects are created with a length data property that cannot be deleted or reconfigured.
  16. Let oldLen be oldLenDesc.[[Value]]. (Note that oldLen is guaranteed to be a unsigned 32-bit integer.)
  17. If P is "length", then a. If the [[Value]] field of Desc is absent, then goto SKIPARRAY. b. Let newLen be ToUint32(Desc.[[Value]]). c. If newLen is not equal to ToNumber(Desc.[[Value]]), goto REJECTRANGE. d. Set Desc.[[Value]] to newLen. e. If newLen >= oldLen, then goto SKIPARRAY. f. Goto REJECT if oldLenDesc.[[Writable]] is false. g. If Desc.[[Writable]] has the value false: 1. Need to defer setting the [[Writable]] attribute to false in case any elements cannot be deleted. 2. Set pendingWriteProtect to true. 3. Set Desc.[[Writable]] to true. h. Goto SKIPARRAY. (Rest of the processing happens in the post-step.)
  18. Else if P is an array index (E5 Section 15.4), then: a. Let index be ToUint32(P). b. Goto REJECT if index >= oldLen and oldLenDesc.[[Writable]] is false. c. Goto SKIPARRAY. (Rest of the processing happens in the post-step.)
  19. SKIPARRAY: Let current be the result of calling the [[GetOwnProperty]] internal method of O with property name P.
  20. Let extensible be the value of the [[Extensible]] internal property of O.
  21. If current is undefined: a. If extensible is false, then goto REJECT. b. If IsGenericDescriptor(Desc) or IsDataDescriptor(Desc) is true, then 1. Create an own data property named P of object O whose [[Value]], [[Writable]], [[Enumerable]] and [[Configurable]] attribute values are described by Desc. If the value of an attribute field of Desc is absent, the attribute of the newly created property is set to its default value. c. Else, Desc must be an accessor Property Descriptor so, 1. Create an own accessor property named P of object O whose [[Get]], [[Set]], [[Enumerable]] and [[Configurable]] attribute values are described by Desc. If the value of an attribute field of Desc is absent, the attribute of the newly created property is set to its default value. d. Goto SUCCESS.
  22. Goto SUCCESS, if every field in Desc also occurs in current and the value of every field in Desc is the same value as the corresponding field in current when compared using the SameValue algorithm (E5 Section 9.12). (This also covers the case where every field in Desc is absent.)
  23. If the [[Configurable]] field of current is false then a. Goto REJECT, if the [[Configurable]] field of Desc is true. b. Goto REJECT, if the [[Enumerable]] field of Desc is present and the [[Enumerable]] fields of current and Desc are the Boolean negation of each other.
  24. If IsGenericDescriptor(Desc) is true, then goto VALIDATED.
  25. Else, if IsDataDescriptor(current) and IsDataDescriptor(Desc) have different results, then a. Goto REJECT, if the [[Configurable]] field of current is false. b. If IsDataDescriptor(current) is true, then 1. Convert the property named P of object O from a data property to an accessor property. Preserve the existing values of the converted property's [[Configurable]] and [[Enumerable]] attributes and set the rest of the property's attributes to their default values. c. Else, 1. Convert the property named P of object O from an accessor property to a data property. Preserve the existing values of the converted property's [[Configurable]] and [[Enumerable]] attributes and set the rest of the property's attributes to their default values. d. Goto VALIDATED.
  26. Else, if IsDataDescriptor(current) and IsDataDescriptor(Desc) are both true, then a. If the [[Configurable]] field of current is false, then 1. Goto REJECT, if the [[Writable]] field of current is false and the [[Writable]] field of Desc is true. 2. Goto REJECT, If the [[Writable]] field of current is false, and the [[Value]] field of Desc is present, and SameValue(Desc.[[Value]], current.[[Value]]) is false. b. Goto VALIDATED.
  27. Else, IsAccessorDescriptor(current) and IsAccessorDescriptor(Desc) are both true so, a. If the [[Configurable]] field of current is false, then 1. Goto REJECT, if the [[Set]] field of Desc is present and SameValue(Desc.[[Set]], current.[[Set]]) is false. 2. Goto REJECT, if the [[Get]] field of Desc is present and SameValue(Desc.[[Get]], current.[[Get]]) is false. b. Goto VALIDATED.
  28. VALIDATED: For each attribute field of Desc that is present, set the correspondingly named attribute of the property named P of object O to the value of the field.
  29. SUCCESS: If O is an Array object: a. If P is "length", and newLen < oldLen, then: 1. Let shortenSucceeded, finalLen be the result of calling the internal helper ShortenArray() with oldLen and newLen. 2. Update the property ("length") value to finalLen. 3. If pendingWriteProtect is true, update the property ("length") to have [[Writable]] = false. 4. Goto REJECT, if shortenSucceeded is false. b. If P is an array index and index >= oldLen: 1. Update the "length" property of O to the value index + 1. This always succeeds, because we've checked in the pre-step that the "length" is writable, and since P is an array index property, the length must still be writable here.
  30. If O is an arguments object which has a [[ParameterMap]] internal property: a. Let map be the value of the [[ParameterMap]] internal property of the arguments object. b. If the result of calling the [[GetOwnProperty]] internal method of map passing P as the argument is not undefined, then: 1. If IsAccessorDescriptor(Desc) is true, then: a. Call the [[Delete]] internal method of map passing P, and false as the arguments. (This removes the magic binding for P.) 2. Else (Desc may be generic or data descriptor): a. If Desc.[[Value]] is present, then: 1. Call the [[Put]] internal method of map passing P, Desc.[[Value]], and Throw as the arguments. (This updates the bound variable value.) b. If Desc.[[Writable]] is present and its value is false, then: 1. Call the [[Delete]] internal method of map passing P and false as arguments. (This removes the magic binding for P, and must happen after a possible update of the variable value.)
  31. Return O.
  32. REJECT: If Throw is true, then throw a TypeError exception, otherwise return false.
  33. REJECTRANGE: Throw a RangeError exception. Note that this is unconditional (thrown even if Throw is false).

Notes:

  • Step 3 is redundant (it comes from ToPropertyDescriptor() because of step 1.
  • Since Throw is always true, step 12 can be removed and step 32 changed to throw TypeError unconditionally. Note that Throw is also given as a parameter in step 30.b.2.1 as an argument for an internal [[Put]] to the parameter map. This actually has no effect on behavior (the internal setter will be called, and the Throw flag is not visible to the setter).

Some cleanup

  1. If Type(O) is not Object throw a TypeError exception.
  2. Let name be ToString(P). (Note: this may have side effects.)
  3. Let desc be a new, empty Property Descriptor.
  4. If O.[[HasProperty]]("enumerable") === true, then set desc.[[Enumerable]] to ToBoolean(O.[[Get]]("enumerable")).
  5. If O.[[HasProperty]]("configurable") === true, then set desc.[[Configurable]] to ToBoolean(O.[[Get]]("configurable")).
  6. If O.[[HasProperty]]("value") === true, then set desc.[[Value]] to O.[[Get]]("value").
  7. If O.[[HasProperty]]("writable") === true, then set desc.[[Writable]] to ToBoolean(O.[[Get]]("writable")).
  8. If O.[[HasProperty]]("get") === true, then: a. Set desc.[[Get]] to O.[[Get]]("get"). b. If desc.[[Get]] !== undefined and IsCallable(desc.[[Get]]) === false, then throw a TypeError exception.
  9. If O.[[HasProperty]]("set") === true, then: a. Set desc.[[Set]] to O.[[Get]]("set"). b. If desc.[[Set]] !== undefined and IsCallable(desc.[[Set]]) === false, then throw a TypeError exception.
  10. If either desc.[[Get]] or desc.[[Set]] are present, then: a. If either desc.[[Value]] or desc.[[Writable]] are present, then throw a TypeError exception.
  11. Set pendingWriteProtect to false.
  12. If O is not an Array object, goto SKIPARRAY.
  13. Let oldLenDesc be the result of calling the [[GetOwnProperty]] internal method of O passing "length" as the argument. The result will never be undefined or an accessor descriptor because Array objects are created with a length data property that cannot be deleted or reconfigured.
  14. Let oldLen be oldLenDesc.[[Value]]. (Note that oldLen is guaranteed to be a unsigned 32-bit integer.)
  15. If P is "length", then a. If the [[Value]] field of Desc is absent, then goto SKIPARRAY. b. Let newLen be ToUint32(Desc.[[Value]]). c. If newLen is not equal to ToNumber(Desc.[[Value]]), goto REJECTRANGE. d. Set Desc.[[Value]] to newLen. e. If newLen >= oldLen, then goto SKIPARRAY. f. Goto REJECT if oldLenDesc.[[Writable]] is false. g. If Desc.[[Writable]] has the value false: 1. Need to defer setting the [[Writable]] attribute to false in case any elements cannot be deleted. 2. Set pendingWriteProtect to true. 3. Set Desc.[[Writable]] to true. h. Goto SKIPARRAY. (Rest of the processing happens in the post-step.)
  16. Else if P is an array index (E5 Section 15.4), then: a. Let index be ToUint32(P). b. Goto REJECT if index >= oldLen and oldLenDesc.[[Writable]] is false. c. Goto SKIPARRAY. (Rest of the processing happens in the post-step.)
  17. SKIPARRAY: Let current be the result of calling the [[GetOwnProperty]] internal method of O with property name P.
  18. Let extensible be the value of the [[Extensible]] internal property of O.
  19. If current is undefined: a. If extensible is false, then goto REJECT. b. If IsGenericDescriptor(Desc) or IsDataDescriptor(Desc) is true, then 1. Create an own data property named P of object O whose [[Value]], [[Writable]], [[Enumerable]] and [[Configurable]] attribute values are described by Desc. If the value of an attribute field of Desc is absent, the attribute of the newly created property is set to its default value. c. Else, Desc must be an accessor Property Descriptor so, 1. Create an own accessor property named P of object O whose [[Get]], [[Set]], [[Enumerable]] and [[Configurable]] attribute values are described by Desc. If the value of an attribute field of Desc is absent, the attribute of the newly created property is set to its default value. d. Goto SUCCESS.
  20. Goto SUCCESS, if every field in Desc also occurs in current and the value of every field in Desc is the same value as the corresponding field in current when compared using the SameValue algorithm (E5 Section 9.12). (This also covers the case where every field in Desc is absent.)
  21. If the [[Configurable]] field of current is false then a. Goto REJECT, if the [[Configurable]] field of Desc is true. b. Goto REJECT, if the [[Enumerable]] field of Desc is present and the [[Enumerable]] fields of current and Desc are the Boolean negation of each other.
  22. If IsGenericDescriptor(Desc) is true, then goto VALIDATED.
  23. Else, if IsDataDescriptor(current) and IsDataDescriptor(Desc) have different results, then a. Goto REJECT, if the [[Configurable]] field of current is false. b. If IsDataDescriptor(current) is true, then 1. Convert the property named P of object O from a data property to an accessor property. Preserve the existing values of the converted property's [[Configurable]] and [[Enumerable]] attributes and set the rest of the property's attributes to their default values. c. Else, 1. Convert the property named P of object O from an accessor property to a data property. Preserve the existing values of the converted property's [[Configurable]] and [[Enumerable]] attributes and set the rest of the property's attributes to their default values. d. Goto VALIDATED.
  24. Else, if IsDataDescriptor(current) and IsDataDescriptor(Desc) are both true, then a. If the [[Configurable]] field of current is false, then 1. Goto REJECT, if the [[Writable]] field of current is false and the [[Writable]] field of Desc is true. 2. Goto REJECT, If the [[Writable]] field of current is false, and the [[Value]] field of Desc is present, and SameValue(Desc.[[Value]], current.[[Value]]) is false. b. Goto VALIDATED.
  25. Else, IsAccessorDescriptor(current) and IsAccessorDescriptor(Desc) are both true so, a. If the [[Configurable]] field of current is false, then 1. Goto REJECT, if the [[Set]] field of Desc is present and SameValue(Desc.[[Set]], current.[[Set]]) is false. 2. Goto REJECT, if the [[Get]] field of Desc is present and SameValue(Desc.[[Get]], current.[[Get]]) is false. b. Goto VALIDATED.
  26. VALIDATED: For each attribute field of Desc that is present, set the correspondingly named attribute of the property named P of object O to the value of the field.
  27. SUCCESS: If O is an Array object: a. If P is "length", and newLen < oldLen, then: 1. Let shortenSucceeded, finalLen be the result of calling the internal helper ShortenArray() with oldLen and newLen. 2. Update the property ("length") value to finalLen. 3. If pendingWriteProtect is true, update the property ("length") to have [[Writable]] = false. 4. Goto REJECT, if shortenSucceeded is false. b. If P is an array index and index >= oldLen: 1. Update the "length" property of O to the value index + 1. This always succeeds, because we've checked in the pre-step that the "length" is writable, and since P is an array index property, the length must still be writable here.
  28. If O is an arguments object which has a [[ParameterMap]] internal property: a. Let map be the value of the [[ParameterMap]] internal property of the arguments object. b. If the result of calling the [[GetOwnProperty]] internal method of map passing P as the argument is not undefined, then: 1. If IsAccessorDescriptor(Desc) is true, then: a. Call the [[Delete]] internal method of map passing P, and false as the arguments. (This removes the magic binding for P.) 2. Else (Desc may be generic or data descriptor): a. If Desc.[[Value]] is present, then: 1. Call the [[Put]] internal method of map passing P, Desc.[[Value]], and true as the arguments. (This updates the bound variable value. Note that the Throw flag is irrelevant, true used now.) b. If Desc.[[Writable]] is present and its value is false, then: 1. Call the [[Delete]] internal method of map passing P and false as arguments. (This removes the magic binding for P, and must happen after a possible update of the variable value.)
  29. Return O.
  30. REJECT: Throw a TypeError exception.
  31. REJECTRANGE: Throw a RangeError exception.