I've read and reviewed few books these years, and few of them quite recently too; none of them went deep enough on how to use these ES5 features that for long time has been avoided ... reasons?
Somebody might say it's an aesthetics problem:
@WebReflection @antirez property descriptors are terrible :( I'm all for optional types in js, but it should be syntax
— TJ Holowaychuk (@tjholowaychuk) March 20, 2014
while somebody else might think these offer poor performance:
.@tjholowaychuk @WebReflection @antirez the most terrible thing about property descriptors is that Object.defineProperty is slow
— Erik Bryn (@ebryn) March 20, 2014
Although I believe is the main reason most developers never ever even investigated about descriptors is that some popular code guide/style written in 2011, the year when ES5 got widely adopted, described new ES5 features as crazy shit:
Object.freeze, Object.preventExtensions, Object.seal
Crazy shit that you will probably never need. Stay away from it.
— Felix Geisendörfer (@felixge) Sometime in 2011
I've already commented that guide in November 2011 and this time I'll try to counter balance that butterfly effect we, as developers and bloggers, should always be aware of when it comes to guidance on programming, right?Accordingly, the goal of these posts is to tell you everything about ES5 descriptors and also do some myth-busters explaining the what, the why, and the when, about such powerful feature that you'll realize it does not look bad, it's not slow at all if done in a certain way, and it's absolutely not some crazy shit, rather something the moment you'll lear more about you probably wish you already knew and put in production 3 years ago ... ready?
Quick Background on ES5
ES5 has been released under the don't break the web motto. Everything you can do in ES5 can be interpreted in old ES3 browsers without Syntax errors, something not possible, as example, with the incoming ES6 features and syntax changes.In this scenario, methods like
Object.defineProperty
where the best place and the best option to not break, syntax speaking, anything around, also leaving developers with the possibility to arbitrary chose to use these methods or not.Few of them could be easily polyfilled, or at least closely simulated.
Long story short, ES5 today is the most widely adopted ECMAScript Standard, present in every Mobile or Desktop Browser, as well as every server side engine.
The only die-hard browser developers keep want to support is IE8, but we'll see later on that we could use this browser too in order to perform most of the tasks that ES5 introduced.
What Is A Descriptor
Let's consider the followingObject
:
var object = { key: 123 };The property
key
can be easily described as enumerable, configurable, and writable, and represented as such through its descriptor:
Object.getOwnPropertyDescriptor(object, 'key'); /* { value: 123, writable: true, enumerable: true, configurable: true } */Above descriptor is the most common one in user land, other common descriptors usually come from native prototypes.
In order to understand how natives are defined in JavaScript, let's have a look on what these properties mean.
enumerable
Properties that are enumerable will always show up in a classicfor/in
loop, as it would be for the key
property in the previous object
.However, all native prototypes are usually defined as such:
Object.getOwnPropertyDescriptor( Object.prototype, 'toString' ); /* { value: [Function: toString], writable: true, enumerable: false, configurable: true } */Not only
new Object
won't show inherited methods in a for/in
loop, all natives will not show up neither if checked directly:
for(var key in Object.prototype) console.log(key); // nothing Object.keys(Object.prototype); // [] // finally some info! Object.getOwnPropertyNames( Object.prototype); [ 'constructor', 'toString', 'toLocaleString', 'valueOf', 'hasOwnProperty', 'isPrototypeOf', 'propertyIsEnumerable', '__defineGetter__', '__lookupGetter__', '__defineSetter__', '__lookupSetter__', '__proto__' ]
Object.getOwnPropertyNames
is the only way to retrieve not-enumerable properties from a generic object. This is also something to keep in mind also when we'd like to simply copy own properties from an object to another.
// how to copy all own properties // **ignoring getters and setters** Object .getOwnPropertyNames(source) .forEach(function (key) { target[key] = source[key]; }) ;
configurable
When a property is configurable, this means we can re-configure ordelete
it.
var object = Object.defineProperty( {}, 'key', { // not enumerable // neither writable configurable: true, value: 123 } ); // reconfiguring Object.defineProperty( object, 'key', { configurable: true, value: 456 } ); object.key; // 456 // removing key delete object.key; // true // also most native properties and methods // can be simply dropped as such delete Object.prototype.__proto__; // true delete Object.prototype.toString; // trueIf we would like to prevent latest snippet to occur, we can either use
Object.seal(Object.prototype)
, ensuring that all properties cannot be removed but also making impossible to add or remove others, which is as obtrusive as changing it since we don't generally own the Object.prototype
, or we could manually loop over all own descriptors and change the configurable part, when necessary, without sealing the whole object:
Object.unconfigurable = (function(){ function reconfigure(key) { var descriptor = Object .getOwnPropertyDescriptor(this, key); if (descriptor.configurable) { descriptor.configurable = false; Object.defineProperty( this, key, descriptor); } } return function unconfigurable(object) { Object .getOwnPropertyNames(object) .forEach(reconfigure, object) ; }; }()); // ... later on ... Object.unconfigurable( Object.prototype); delete Object.prototype.toString; // falseHowever, this won't guarantee that properties cannot be overwritten, since the
writable
property of the descriptor, is not affected by Object.seal
, neither by above snippet.
writable
Whileconfigurable
is about the ability to delete
or configure a property, writable
is about the ability to change its defined value.
var object = { key: 123 }; var descriptor = Object .getOwnPropertyDescriptor(object, 'key') // setting as non writable descriptor.writable = false; Object.defineProperty( object, 'key', descriptor); // this will fail object.key = 456; object.key; // 123 // remember, writable // is not about the ability // to reconfigure it descriptor.writable = true; Object.defineProperty( object, 'key', descriptor); object.key = 456; object.key; // 456 // neither about the inability // to remove it delete object.key; // true object.key; // undefinedGenerally used to create
constant
like properties, writable
is also the most misleading property when it comes to prototypal inheritance since it creates a trap very similar, conceptually speaking, to the following one:
// concept: similar behavior of // {writable: false, value: 123} var a = Object.defineProperty({}, 'val', { get: function () { return 123; }, set: function () { // only under "use strict" it throws if (function(){return !this}()) { throw new Error('val is non writable'); } } }); a.val = 456; a.val; // 123 var b = Object.create(a); b.val = 456; b.val; // 123 // it's configurable as own property anyway Object.defineProperty( b, 'val', {value: 456}); b.val; // 456While this behavior might look very undesired for properties, it ensures that inherited methods cannot be changed at any time anywhere in the prototypal chain which is actually usually the most secure behavior we could ask for.
When a descriptor has
writable
property, this cannot also contain get
or a set
one.
get & set
When one of these two is present, the descriptor is consider an accessor one instead of a data one, as it is awritable
one which assume also a value
which is by default undefined
.configurable
and enumerable
have exactly same meaning, and here an example of getters:
var arrayLike = Object.defineProperty( {}, 'length', { get: function () { var keys = Object.keys(this); return keys.length && Math.max.apply(null, keys) + 1; } }); arrayLike.length; // 0 arrayLike[0] = 1; arrayLike[1] = 2; arrayLike.length; // 2Getters and setters have been around for a while so I won't talk much about what we can do with these two features, just remember that these cannot coexist with
writable
and value
since that won't make sense (we can simply ignore the set
to avoid writing or throw, to mimic writable:false
behavior and return whatever we want via get
to mimic the value
)
Are Descriptors Inherited ?
I will cover how to use descriptors through prototypal inheritance and how powerful these are but the simple answer to this topic is: NO, descriptors are never inherited, but whatever behavior is defined in the prototype, as a classic non-enumerable could be, will be reflected regularly through all instances ... not because these will have magically a non enumerable property, simply because the moment their prototype is accessed, this won't show such property as enumerable, same way happens with nativeObject.prototype
as explained at the beginning of this post.
Object Use Cases
Dropping useful cases based on inheritance, here a combination of common use cases per each property descriptor:- enumerable is the default choice for all properties defined as literal and it's very useful when debugging in console. An object with non enumerable properties won't reveal itself as good as one with enumerable properties ... as example
{a:123}
will be shown as such in console whileObject.defineProperty({},'a',{value:123})
won't. - configurable gives the ability to both
delete
and redefine a property through a different descriptor. Latter one is less common case but very powerful combined with lazily defined properties. - writable, combined with
configurable
, is the only way we have to define constants like properties.Object.defineProperty(Class, 'CONST', {value: 'immutable'})
is a very nice and safe feature we can use without problems anywhere we want to. - get and set are a very transparent way to validate properties or link private methods or variables together. These can also be used as lazy properties, when accessing them should calculate once something expensive, instead of penalizing at runtime through a multitude of similar computations.
Performance
Still removing inheritance related patterns from the equation, these are single property results comparing direct property set VSObject.defineProperty
, while these are multiple properties at once via Object.defineProperties
VS normal object literal.It's good to have these bench in, but it's very early to conclude that you should not use these ES5 features ... we are still missing all potentials once descriptors are integrated with prototypal inheritance.
Don't miss the next part of the pot ;)
You should pass `object` reference instead of new object in second `defineProperty` call in reconfiguring example.
ReplyDeleteYou wrote:
ReplyDelete"Everything you can do in ES5 can be interpreted in old ES3 browsers without Syntax errors, something not possible, as example, with the incoming ES6 features and syntax changes."
But what about the syntax for getters and setters in object literals? It's ES5, isn't it?
fixed, thanks !
ReplyDelete@thorn kinda good point and I have two answers for that:
ReplyDelete1) it's not by accident there's not a single example using them so far .. this is Part 1, you have to be patience and I'll cover way more than that
2) that wasn't breaking syntax because those were already a de-facto standard, same as __defineGetter__ ... JScript, which wasn't based on ECMAScript 3, was the only one not supporting them
Stay tuned, this evening I'll probably publish Part 2