My JavaScript book is out! Don't miss the opportunity to upgrade your beginner or average dev skills.

Monday, March 24, 2014

What Books Didn't Tell You About ES5 Descriptors - Part 1

Once you've read, Part 2 is out ;-)
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: while somebody else might think these offer poor performance: 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: 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 following Object:
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 classic for/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 or delete 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;  // true
If 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; // false
However, 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

While configurable 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;  // undefined
Generally 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; // 456

While 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 a writable 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; // 2
Getters 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 native Object.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 while Object.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 VS Object.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 ;)

4 comments:

  1. You should pass `object` reference instead of new object in second `defineProperty` call in reconfiguring example.

    ReplyDelete
  2. You wrote:
    "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?

    ReplyDelete
  3. @thorn kinda good point and I have two answers for that:

    1) 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

    ReplyDelete

Note: Only a member of this blog may post a comment.