ECMAScript 5: a revolution in object properties
Over the coming weeks I'm going to focus on discussing the mini revolution that ECMAScript 5 brought, and the implications in particular for objects and their properties.
ECMA5's final draft was published at the end of 2009, but it was only really when IE9 launched in early 2011 - and, with it, impressive compatibility for ECMA5 - that it became a genuinely usable prospect. Now in 2012, it is being used more and more as browser vendors support it and its power becomes apparent. (Full ECMA5 compatibility table).
JavaScript has always been a bit of an untyped, unruly free-for-all. ECMAScript 5 remedies that somewhat by giving you much greater control over what, if anything, can happen to object properties once changed - and it's this I'll be looking at in this first post.
A new approach to object properties
In fact the whole idea of an object property has changed; it's no longer a case of it simply being a name-value pairing - you can now dictate all sorts of configuration and behaviour for the property. The new configurations available to each property are:
- value - the property's value (obviously)
- writable - whether the property can be overwritten
- configurable - whether the property can be deleted or have its configuration properties changed
- enumerable - whether it will show up in a for-in loop
- get - a function to fire when the property's value is read
- set - a function to fire when the property's value is set
Collectively, these new configuration properties are called a property's descriptor. What's vital to understand, though, is that some are incompatible with others.
Two flavours of objects
The extensive MDN article on ECMAScript 5 properties suggests thinking of object properties in two flavours:
- data descriptors - a property that has a value. In its descriptor you can set value and writable but NOT get or set
-
accessor descriptors - a property described not by a value but by a pair of getter-setter functions. In its descriptor you can set get and set but NOT value or writable.
Note that enumerable and configurable are usable on both types of property. I'm struggling to understand why someone thought the ability to set a value and a setter function, for example, were incompatible desires. If I find out, I'll let you know.
New methods
To harness this new power, you need to define properties in one of three ways - all stored as new static methods of the base Object object:
- defineProperty()
- defineProperties()
- create()
The first two work identically except the latter allows you to set multiple properties in one go. As for Object.create(), I'll be covering that separately in a forthcoming post.
Object.defineProperty() is arguably the most important part of this new ECMAScript spec; as John Resig points out in his post on the new features, practically every other new feature relies on this methd.
Object.defineProperty() accepts three arguments:
- the object you wish to add a property to
- the name of the property you wish to add
- a descriptor object to configure the property (see descriptor properties above)
Let's see it in action.
1var obj1 = {};
2Object.defineProperty(obj1, 'newProp', {value: 'new value', writable: false});
3obj1.newProp = 'changed value';
4console.log(obj1.newProp); //new value - no overwritten
See how the overwrite failed? No error or warning is thrown - it simply fails silently. In ECMA5's new 'strict mode', though, it does throw an exception. (Thanks to Michiel van Eerd for pointing this out.)
There we set a data descriptor. Let's set an accessor descriptor instead.
1var obj = {}, newPropVal;
2Object.defineProperty(obj, 'newProp', {
3 get: function() { return newPropVal; },
4 set: function(newVal) { newPropVal = newVal; }
5});
6obj.newProp = 'new val';
7console.log(obj.newProp); //new val
You might be wondering what on earth is going on with that newPropVal variable. I'll come to that in my next post which will look at getters and setters in detail. Note also how, with our setter, the new value is forwarded to it as its only argument, as you'd expect.
The fact that these properties can be set only via these methods means you cannot create them by hand or in JSON files. So you can't do:
1var obj = {prop: {value: 'some val', writable: false}}; //etc
2obj.prop = 'overwritten'; //works; it's not write-protected
ECMA 5 properties don't replace old-style ones
An important thing to understand early on is that this new form of 'uber' property is not the default. If you define properties in the old way, they will behave like before.
1var obj = {prop: 'val'};
2obj.prop = 'new val'; //overwritte - no problemo
Reporting back
Note that these new configuration properties are, once set, not available via the API; rather, they are remembered in the ECMAScript engine itself. So you can't do this (using the example above):
console.log(obj1.newProp.writable); //error; newProp is not an object
Instead, you'll be needing Object.getOwnPropertyDescriptor. This takes two arguments - the object in question and the property you want to know about. It returns the same descriptor object you set above, so something like:
{value: 'new value', writable: true, configurable: true, enumerable: true}
More to come..
So there you go - a very exciting mini revolution, as I said. This new breed of intelligent object property really is at the heart of arguably the most major shake-up to the language for a long time. Next week I'll continue this theme - stay posted!
10 comments | post new
A friendly alternative to currying
I recently did a small talk and demonstration on currying in JavaScript. My personal take is that, while occasionally useful, it's a bit of a nuclear option, for limited reward.
Er, what's currying?
If you don't know, currying involves what's called partial application: invoking functions or methods whilst omitting some of the arguments, for which default values are assumed instead.
In other languages this is simpler. In PHP, for example, you can specify default values at the point of declaring your function:
1
2function someFunc($arg1 = 'default val') { echo $arg1; }
3someFunc(); //default val
4?>
Since that's not possible in JavaScript, the typical workaround is to feed your function to a curry-fier function, along with some default values, and get back a new version of your function that will assume default values for any omitted arguments. It might look something like this:
1//my simple function
2function add(num1, num2) { return num1 + num2; }
3
4//a curry-fier function
5function curryfy(func) {
6 var slice = Array.prototype.slice,
7 default_args = slice.call(arguments, 1);
8 return function() {
9 var args = slice.call(arguments);
10 for (var i=0, len=default_args.length; i 11 if (default_args[i] && (args[i] === null || args[i] === undefined)) 12 args[i] = default_args[i]; 13 return func.apply(this, args); 14 }; 15} 16 17//re-engineer our function to assume default values for omitted args 18var add = curryfy(add, 13, 14);
It's a little beyond the scope of this article to explain line-by-line what's going on there, but needless to say I can now call my add function with only some, or even no arguments.
1add(3, 4); //7
2add(); //27 - default values of 13 and 14 used
Drawbacks
That's all very nice. BUT, there are drawbacks and pitfalls to curry-fying. For one thing, a curryfied function no longer behaves as its originally-declared former self suggests. This might well throw a developer that comes to your code anew - particularly since JavaScript, as I say, does not support natively any notion of default function arguments.
Another problem concerns hoisting. If your function is a traditional one, i.e. as opposed to an anonymous function, the function itself will be hoisted, but its curryfied redefinition will not be (since curryfying a function always means declaring it as an anonymous function. For example:
1add(); //NaN - no values. Curryfied func doesn't exist yet.
2function add(num1, num2) { return num1 + num2; }
3var add = curryfy(add, 13, 14); //this time we redefine add as an anonymous function
4add(); //27
Our function is hoisted, since it's a traditional function not an anonymous one. That's the reason we can call it before the line where it's declared. However its curryfied redefinition will not be hoisted - so we would need to either curryfy it higher in the page or move our call to the function down.
A friendly alternative
My approach is rather different. Instead of redefining the function, let's ensure that it - and, indeed, every function - automatically inherits an extension to itself that allows us to omit variables. In addition, we'll set up another extension, called after the function is declared, that lets us set the default values. So the end result would be:
1function dog(breed, name) { alert(name+' is a '+breed); } //the func
2dog.setDefaults('poodle', 'Fido'); //set default value(s)
3dog('alsatian', 'Rex'); //call original version
4dog.withDefaults(null, 'Rex'); //call curryfied version
Hopefully it's clear what's happening there. I declare my function, then immediately stipulate what the default values are. Then, to invoke my function in such a way that I can have those default values substitute any omitted arguments, I call an extension of my function, called withDefaults(). To this I pass partial or even no arguments at all.
An advantage of this is that it doesn't destroy our original function, so anyone coming to our code anew and expecting traditional, not curryfied behaviour from our function, won't be surprised or confused.
Here's the code behind this approach:
1//function extension: allow version that uses default values
2Function.prototype.withDefaults = Function.withDefaults = function() {
3 var args = Array.prototype.slice.call(arguments);
4 if (this.defaults && this.defaults instanceof Object)
5 for (var i=0, len=this.defaults.length; i 6 if (this.defaults[i] && (args[i] === null || args[i] === undefined)) 7 args[i] = this.defaults[i]; 8 this.apply(this, args); 9 return (function(outerThis) { return function() { outerThis.apply(this, args); }; })(this);(this); 10} 11 12//function extension: set default values 13Function.prototype.setDefaults = function() { this.defaults = arguments; return this; }
As you can see, I'm declaring these two extensions on the Function object's prototype, so I can be sure they are both inherited by any and all functions I create.
I'll talk through the code in a separate blog post, but here's some usage demos for it, showing you different ways it can be used.
1//example 1: with traditional function
2function dog(name, breed) {
3 console.log(name+' is a '+breed);
4};
5dog.setDefaults(null, 'Daschund');
6dog.withDefaults('Fido');
7dog('Fido', 'spaniel');
8
9//example 2: with anonymous function
10var dog = function(name, breed) {
11 console.log(name+' is a '+breed);
12}.setDefaults('Jeremy', 'Bassetthound');
13dog.withDefaults('Kevin');
14dog('Josh', 'Bassetthound');
15
16//example 3: with instantiation
17var Dog = function (name, breed) {
18 this.name = name; this.breed = breed;
19}
20Dog.setDefaults('Russel', 'terrier');
21var dog1 = new (Dog.withDefaults());
22var dog2 = new Dog('Taz', 'labrador');
23console.log(dog1.name+' is a '+dog1.breed);
24console.log(dog2.name+' is a '+dog2.breed);
The output from that will be:
Example 1
1Fido is a Daschund
2Fido is a spaniel
Example 2
1Kevin is a Bassetthound
2Josh is a Bassetthound
Example 3
1Russel is a terrier
2Taz is a labrador
There's several interesting points of note there:
- When setting defaults for a function, you can specify values for as few or as many arguments as you like - it doesn't have to be all of them.
- When dealing with anonymous functions (example 2), you can call setDefaults() by chaining it to the end of your function literal.
- The approach even works with OO / instantiation, as shown in example 3. A lot of curryfyer scripts overlook this use case.
Phew. So, in summary?
You have a function. It accepts a lot of arguments. You find yourself passing many of the same arguments to it each time. Using the above script you can set some default values and then not have to worry about passing them as arguments when you call the function in future.
post a commentPresenting XMLTree v.2.0
You may remember XMLTree, which I launched several months ago. In short, it's a plugin that generates a tree from XML data, allowing you to traverse and interrogate the data but also bind events to the various branches of the tree.
Version 2.0 is now here, and comes with a host of powerful new features, including:
Sub-trees: loading data from a web service
Sub-trees allow you to load data branch-by-branch rather than all at once. This means you can use XMLTree with a web service, which serves up the root-level data on load, and then branch-specific data as and when branches are expanded.
Imagine you have a tree showing the boroughs of London. When a branch is expanded, it should show all the bus numbers that serve that borough.
Let's assume the web service returns the following XML format for the root data:
1
2
3
4
5
6
Knowing that, here's our tree instantiation:
1var ws_url = 'my_web_service.php';
2new XMLTree({
3 fpath: ws_url,
4 container: '#tree',
5 attrsAsData: 'borough_id',
6 subTreeBranches: true,
7 subTreeRequest: function(li) {
8 return ws_url+'?borough_id='+li.data('borough_id');
9 }
10});
By using the attrsAsData param (see below), we ensure that any borough_id attributes from the XML are transfered over as element data onto the tree LIs.
That's critical, because it means that, when a branch is expanded, we can look up that piece of data to determine URL of the web service for loading the next set of data.
The URL is determined and returned by the subTreeRequest param, a callback function that fires when a sub-tree-able branch is expanded and to which the expanded branch's LI is automatically passed. So the URL returned will be something like my_web_service.php?borough_id=13
In summary: to start with, only the root-level data is loaded. The data that goes within branches is determined only when a branch is expanded, by making a separate call to the web service. Crucial information is passed to the web service so it knows what data is returned - in this case, the ID of the clicked branch.
Transfer XML attributes to classes/element data
Attributes in your XML can now be transfered over to the resultant mark-up - in the form of either classes or element data attributes on the branch LIs.
In either case, you can either specify that all attributes should be transferred over, or just some.
Other features
Other, more minor features in this release include:
- support for loading XML over JSONP internet requests
- ability to auto-open the tree at a specific point once it's loaded
- ability to output, ignore or output but hide attributes as branches
Head over here to download, get usage info or view a demo.
post a comment