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

Sunday, January 20, 2013

redefine.js - A Simplified ES5 Approach

If you think Object.defineProperty() is not widely adopted, here the list of browsers that support it, together with Object.create() and Object.defineProperties().
  • Desktop
    • Chrome 7+
    • Firefox 4+
    • Safari 5+
    • Opera 12+
    • IE 9+
  • Mobile
    • Android 2.2+, 3+, and 4+, stock browser
    • Android 4.1+ Chrome
    • Android and Symbian Opera Mobile
    • Android Dolphin Browser
    • Android Firefox
    • iOS 4+, 5+, and 6+, Safari Mobile
    • iOS Chrome Browser
    • Windows Phone 7+ IE9 Mobile
    • Windows 8+ RT and Pro
    • webOS stock Webkit browser
    • Chrome OS
    • Firefox OS
    • I believe Blackberry supports them without problems with their advanced browser
    • I believe updated Symbian too but I could not test this
  • Server
    • node.js
    • Rhino/Ringo
    • JSC
    • BESEN and others I could not test too
Except where I have stated differently, I have manually tested everything in this list but you can try by your self in kangax es5 compat table.
Please note that even if Object.freeze() and others might not be supported, create, defineProperty, and defineProperties are, as it is for example in Android 2.2 stock browser.
As summary, unless you are not so unfortunate you have to support that 8% (and dropping) of IE8 market share, there are really no excuse to keep ignoring JavaScript ES5 Descriptors.
Update the build process now updates tests automatically in the repository web page.

Descriptors Are Powerful

Things we can improve using ES5 descriptors are many, including new patterns we never even thought were possible since most of them might result not implementable in many other programming languages.
This is as example the case of the inherited getter replaced on demand with a direct property access, a pattern discussed in The Power Of Getters post, a pattern described from jonz as:
Right now this syntax seems like obfuscation but the patterns it supports are what I've always wanted, I wonder if it will ever become familiar.

Descriptors Are Weak Too

Not only the syntax might look completely not familiar for everything that has been written until now in JavaScript, but current specifications suffer inheritance problems. Consider this piece of malicious code:
Object.prototype.enumerable = true;
Object.prototype.configurable = true;
And guess what, every single property defined without specifying those properties, both false as default, will be enumerable and deletable so for/in loops and trustability will be both compromise.
Even worst, if Object.prototype.writable = true comes in the game, every attempt to define a getter or a setter will miserably fail.
I don't think we are planning to write this kind of code to ensure desired defaults, right?
Object.defineProperties(
  SomeClass.prototype, {
  prop1: {
    enumerable: false,
    writable: false,
    configurable: false,
    value: "prop1"
  },
  method1: {
    enumerable: false,
    writable: false,
    configurable: false,
    value: function () {
      return this.prop1;
    }
  }
});
Not only the moment we would like to assign a getter, we are trapped in any case, since we cannot have both writable and get, even if inherited, but above code has been always written in JS such:
SomeClass.prototype = {
  prop1: "prop1",
  method1: function () {
    return this.prop1;
  }
};
OK, this way will enforce us to use Object#hasOwnProperty() in every loop and does not guarantee that those properties won't change in the prototype, but how about having the best from both worlds?

redefine.js To The Rescue

This tiny library goal, which size once minzipped is about 650 bytes, is to use the power of ES5 descriptors in an easier, memory safe, and more robust approach.
redefine(
  SomeClass.prototype, {
  prop1: "prop1",
  method1: function () {
    return this.prop1;
  }
});
That's pretty much it, an ES3 alike syntax with ES5 descriptors and the ability to group definitions by descriptor properties.
redefine(
  SomeClass.prototype, {
  prop1: "prop1",
  method1: function () {
    return this.prop1;
  }
}, {
  // we want that prop1 and method1
  // can be changed runtime in the prototype
  writable: true,
  // we also want them to be configurable
  configurable: true
  // if not specified, enumerable is false by default
});
As easy as that, that group of properties will all have those descriptor behavior and everything is safe from the Object.prototype, you have 50 and counting tests for the whole library that should cover all possibilities with the provided API.

More Power When Needed

What if we need to define a getter inline? How to not have ambiguity problems since the value is accepted directly? Like this :)
redefine(
  SomeClass.prototype, {
  get1: redefine.as({
    get: function () {
      return 123;
    }
  }),
  prop1: "prop1",
  method1: function () {
    return this.prop1;
  }
}, {
  writable: true,
  configurable: true
});
That's correct, redefine can understand developers intention thanks to a couple of hidden classes able to trivially remove ambiguity between a generic value and a meant descriptor, only when needed, as easy way to switch on ES5 power inline.
Wen we need a descriptor? We set a descriptor!
If the descriptor has properties incompatible with provided defaults, latter are ignored and discarded so we won't have any problems, as it is as example defining that getter with writable:true as specified default.

Much More In It!

There is a quite exhaustive README.md page plus a face 2 face in the HOWTO.md one.
Other 2 handy utilities such redefine.from(proto), a shortcut of Object.create() using descriptors and defaults as extra arguments, and redefine.later(callback), another shortcut able to easily bring the lazy getter replaced as property pattern in every developers hands, are both described and fully tested so I do hope you'll appreciate this tiny lib effort and start using it for more robust, easier to read and maintain, ES5 ready and advanced, client and server side projects.
Last, but not least, redefine.js is compatible with libraries such Underscore.js or Lo-Dash, being an utility, rather than a whole framework.

No comments: