Home & blog  /  Tag: object  /

JavaScript getters and setters: varying approaches

posted: 13 Mar '12 17:50 tags: JavaScript, ECMA5, object, responsive UI

Last week I posted an introductory article on ECMAScript 5 object properties, and the mini-revolution that I think they constitute. (The post made the coveted JavaScript Weekly - thanks, guys.)

One of the key features of them is the ability to define getter/setter callbacks on them.

Getters and setters are a means of providing an arm's-length way of getting or setting certain data, whilst keeping private other data, and are common of most languages. In JavasScript, setters are also a good way of ensuring your UI stays up to date as your data changes, which I'll show you an example implementation further down.

A new approach to getters and setters

The new approach looks like this, and can be used only on properties created via the new Object.create() and Object.definePropert[y/ies]() methods.

1var dog = {}, name;

2var name;

3Object.defineProperty(dog, 'name', {

4     get: function() { return name; },

5     set: function(newName) { name = newName; }

6});

7dog.name = 'Fido';

8alert(dog.name); //Fido

You'll note that this approach requires the help of a 'tracker variable' (in our case name) via which the getter/setter reference the property's value. This is to avoid maximum recursion errors that the following would cause:

1...

2     get: function() { return this.name; }, //MR error

3...

That happens because we set a getter, via which any attempt to read the property is routed. Therefore, having the getter reference this.name is effectively asking the getter to call itself - endlessly. Likewise for a setter, if it tried to assign to this.name.

Since each property needs its own tracker, and you don't want lots of variables flying around, it's a good idea to use a closure when declaring several properties.

1var dog = {}, props = {name: 'Fido', type: 'spaniel', age: 4};

2for (var prop in props)

3     (function() {

4         var propVal = props[prop];

5         Object.defineProperty(dog, prop, {

6             get: function() { return propVal; },

7             set: function(newVal) { propVal = newVal; }

8         });

9     })()

10alert(dog.name+' is a '+dog.type); //Fido is a spaniel

11dog.name = 'Rex';

12alert(dog.name+' is age '+dog.age); //Rex is age 4

There, we declare what properties we want on our object, and some start values. The loop sets each property, and tracks its value via a private propVal variable in its closure.

One of the things I like about this new approach is you no longer have to call the getters/setters explicitly (as you did with previous implementations - see below) - they fire simply by talking to the property.

Admittedly this has its proponents and its opponents; those in favour say getters/setters should fire simply by calling/assigning to the property - not calling some special methods to do that. Those against normally point out that someone new to the code might be surprised to find that talking to a property in fact fires a function.

My take is that, as long as this is part of the spec, and your code is well documented, there can be few complaints with using the new implementation.

Other ways of doing getters/setters

In any case, I much prefer them to the implementation we got in JavaScript 1.5.

1var dog = {

2     type: 'Labrador',

3     get foo() { return this.type; },

4     set foo(newType) { this.type = newType; }

5};

6alert(dog.type); //Labrador

7dog.foo = 'Rotweiller';

8alert(dog.foo); //Rotweiller

I've never been in love with this approach, chiefly because you don't deal directly with the property but with a proxy that represents its getter/setter callbacks - in the above example foo. The new approach does away with this; you call/assign to the property just as you would if there were no getters/setters in play, and the getter/setter callbacks kick in automatically - they are not referenced explicitly.

That said, one good point about this separation of property value from getter/setter is that the getter/setter can safely reference the property via this without the risk of recursion error, as befalls the new approach.

The older way

There's also the depracated __defineGetter__() and __defineSetter__() technique.

1var dog = {

2     type: 'Labrador'    

3};

4dog.__defineGetter__('get', function() { return this.type; });

5alert(dog.get); //Labrador

Once again you have to name your setters/getters. By far the most notable point about this approach, though, is you can assign getters/setters after assigning the property - not a super common desire, but useful any time you don't want to or can't alter the prototype. The other two implementations don't allow you to do this, at least without a lot of reworking.

A final point about these latter implementations is that they don't hijack control of your property like the new implementation does. That is, if a developer ignores them and manipulates the property directly, they can. This is not good news; if you defined getters/setters, you probably want them to run, not be bypassed.

1var dog = {

2     name: 'Henry',

3     set foo(newName) { alert('Hi from the setter!'); this.name = newName; }

4};

5dog.name = 'Rex'; //setter bypassed; its alert doesn't fire

Setters and a responsive UI

As I mentioned in the intro, another role of getters in JavaScript can be to keep your UI up to date as your data changes. Frameworks like Backbone JS sell themselves heavily on this concept.

As the intro to the Backbone documentation points out, medium-large JavaScript applications can easily get bogged down with jQuery selectors and other means trying to keep your views in-sync with your data.

A getter can help here. Here's something I cooked up:

1Object.UIify = function(obj) {

2     for(var property in obj) {

3         var orig = obj[property];

4         (function() {

5             var propVal;

6             Object.defineProperty(obj, property, {

7                 get: function() { return propVal; },

8                 set: (function(target) {

9                     return function(newVal) {

10                         propVal = newVal;

11                         $(target).text(propVal);

12                     };

13                 })(orig.target)

14             });

15         })()

16         obj[property] = orig.val;

17     }

18     return obj;

19};

20    

21$(function() {

22     var dog = Object.UIify({name: {val: 'Fido', target: '#name'}, type: {val: 'Labrador', target: '#type'}});

23     dog.name = 'Bert';

24     dog.type = 'Rotweiller';

25});

And here's some example HTML:

<p id='dog'>Hi - my name's <span id='name'></span> and I'm a <span id='type'></span>!</p>

I'll go into the details of what my method does in a further post. Essentially, though, what's happening is we pass an object to the UIify() method where each property is a sub-object containing its starting value (val) and a CSS/jQuery selector pointing to to the UI element that should be updated as and when the value changes (target.)

UIify() then returns an object using the new ECMA5 getters/setters. Whenever a property of the object is overwritten, the corresponding UI element denoted by the target we specified is updated. In my case, the targets were simply elements with IDs, but it could of course be more complex targets - it's just CSS/jQuery selector syntax.

---------

So there you have it, three approaches through the ages. Next time up I'll be looking more at the new Object funcionality in ECMA5.

(p.s. for further reading, be sure to check out the extensive MDN article on working with objects, which talks a lot about getter/setter techniques.)

8 comments | post new

ECMAScript 5: a revolution in object properties

posted: 29 Feb '12 20:03 tags: JavaScript, ECMA5, object

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