Wednesday, October 19, 2011

Do You Really Know Object.defineProperty ?

I am talking about enumerable, configurable, and writable properties of a generic property descriptor.

enumerable

most likely the only one we all expect: if false, a classic for/in loop will not expose the property, otherwise it will. enumerable is false by default.

writable

just a bit more tricky than we think. Nowadays, if a property is defined as non writable, no error will occur the moment we'll try to change this property:

var o = {};
Object.defineProperty(o, "test", {
writable: false,
value: 123
});
o.test; // 123
o.test = 456; // no error at all
o.test; // 123

So the property is not writable but nothing happens unless we try to redefine that property.

Object.defineProperty(o, "test", {
writable: false,
value: 456
});
// throws
// Attempting to change value of a readonly property.

Got it ? Every time we would like to set a property of an unknown object, or one shared in an environment we don't trust, either we use a try/catch plus double check, or we must be sure that Object.getOwnPropertyDescriptor(o, "test").writable is true.
writable is false by default too.

configurable

This is the wicked one ... what would you expect from configurable ?
  • I cannot set a different type of value
  • I cannot re-configure the descriptor
Fail in both cases since things are a bit different on real world. Take this example:

var o = Object.defineProperty({}, "test", {
enumerable: false,
writable: true,
configurable: false, // note, it's false
value: 123
});

Do you think this would be possible ?

Object.defineProperty(o, "test", {
enumerable: false,
writable: false, // note, this is false only now
configurable: false,
value: "456" // note, type and value is different
});

// did I re-configure it ?
o.test === "456"; // true !!!

Good, so a variable that is writable can be reconfigured on writable attribute and on its type.
The only attribute that cannot be changed, once flagged as configurable and bear in mind that false is the default, is configurable itself plus enumerable.
Also writable is false by default.
This inconsistency about configurable seems to be pretty much cross platform and probably meant ... why ?

Brainstorming

If I can't change the value the descriptor must be configurable at least on writable property ... no wait, if I can set the value as not writable then configurable should be set as false otherwise it will loose its own meaning ... no, wait ...

How It Is

writable is the exception that confirms the rule. If true, writable can always be configurable while if false, writable becomes automatically not configurable and the same is true for both get and set properties ... these acts as writable: false no matters how configurble is set.

How Is It If We Do Not Define


// simple object
var o = {};

// simple assignment
o.test = 123;

// equivalent in Object.defineProperty world
Object.defineProperty(o, "test", {
configurable: true,
writable: true,
enumerable: true,
value: 123
});

Thanks to @jdalton to point few hints out.

As Summary

The configurable property works as expected with configurable itself and only with enumerable one.
If we think that writable has anything to do with both of them we are wrong ... at least now we know.

4 comments:

Dmitry A. Soshnikov said...

"Do You Really Know Object.defineProperty ?"

Yep ;)

So the property is not writable but nothing happens unless we try to redefine that

Note, that the a similar (but vice-versa!) situation is with shadowing a non-writable (prototype) property via assignment -- it won't be shadowed, and throws in strict mode. However, you may shadow it in this case with using defineProperty.

Also on "nothing happens" -- it's only in non-strict mode. In strict mode it throws if you try to assign to non-writable property.

Good, so a variable that is writable can be reconfigured on writable attribute and on its type. ... This inconsistency about configurable seems to be pretty much cross platform and probably meant

It's not an inconsistency though, everything's by the spec (so we can claim on the spec ;)). And yes, an important note that a writable attribute on a non-configurable property can be re-set only from true to false, but not vice-versa.

Also notice, you can't change the type of a non-configurable property from data to the accessor.

Another trap, that you can't change get/set of an non-configurable accessor-property (as you can in case of data-property):

var foo = Object.defineProperty({}, "bar", {
get: function () {
return "bar";
}
});

Object.defineProperty(foo, "bar", { // ERROR
get: function () {
return "baz";
}
});


But, in case if those are the same function, you to re-assign (the same) value.

Detailed info on property descriptors is here: http://dmitrysoshnikov.com/ecmascript/es5-chapter-1-properties-and-property-descriptors/

Dmitry.

Andrea Giammarchi said...

hey Dmitri, minifiers remove "use strict" but thanks for extra infos. It wasn't probably a post for you ;)

joseanpg said...

I think it's an unfortunate inconsistency in the specification:

- In 8.6.1 we can read: "[[Configurable]]: If false, attempts to delete the property, change the property to be an accessor property, or change its attributes (other than [[Value]]) will fail."

- However 8.12.9->10.a leaves the door open to the case writable==true becomes writable==false.

Clearly, at this point, [[DefineOwnProperty]] is in contradiction with the spirit of the configurable attribute.

DBJDBJ said...

Yes, there are more "unfortunate inconsistencies" in the speck (pun intended) and implementations ...

var s = "ABC"; s[1] = "X";

Show to any *non* JS programer what is the value of s, after the above.
Then talk to her about the reasons why is this allowed, about browsers, w3c, ES5 etc ... I am sure she will stay out if it all ...