Home & blog  /  2012  /  Mar  /  view post  /

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

post new comment

Comments (8)

Axel Rauschmayer, at 14/03/'12 16:51, said:
The recursion happens, because the getter calls itself! If you use a property name that is different from the getter’s name, you won’t have any trouble and don’t have to store data in an environment. E.g.: a getter "name" can use the property this._name.

Furthermore, an object initializer syntax for defining getters and setters is part of ECMAScript 5 (§11.1.5). For example:

    var obj = {
        get foo() {
            return "getter";
        },
        set foo(value) {
            print("setter: "+value);
        }
    };
Mitya, at 14/03/'12 17:14, said:
 - thanks for your comment. Re: recursion - yes, precisely - I was just pointing out that the previous implementations of getters/setters did use the 'this' keyword when returning or assigning to the property inside the callbacks.
Axel Rauschmayer, at 15/03/'12 10:08, said:
As far as I can see, “the older form of getters/setters that we got in JavaScript 1.5” is standard ECMAScript 5, though. Why would you not want to use it? Often, one just uses getters that compute something and thus don’t need backup storage via a property.

Furthermore, you can use `this` with defineProperty getters:

    > var obj = { bar: "bar" };
    > Object.defineProperty(obj, "foo", { get: function() { return this.bar } })
    > obj.foo
    'bar'
MItya, at 15/03/'12 10:36, said:
I just prefer the latest approach. Perhaps 'older' techniques was the wrong word (though not in the case of __defineGetter__(), obviously). Yes, you can use 'this' in the new implementation for properties already in the object - I was just illustrating that you couldn't use it to reference the property you are defining the getter and/or setter for, which is something less experienced developers may trip over.

var obj = {};
Object.defineProperty(obj, 'prop', {
set: function(newVal) { this.prop = newVal; }
});
obj.prop = 'some val';

...which is recursive and therefore errors, as discussed.
Diego Castorina, at 17/03/'12 12:03, said:
Very nice and informative post, along with the previous one. Thanks!
Ryan Gasparini, at 19/03/'12 15:53, said:
How would you use this in production code? Sure this may seem like a good idea if you have only one object. But what happens IRL where you may have hundreds of objects?

var cat = {}, dog = {}, name;
var name;
Object.defineProperty(dog, 'name', {
    get: function() { return name; },
    set: function(newName) { name = newName; }
});
dog.name = 'Fido';
console.log(dog.name); // Fido

Object.defineProperty(cat, "name", {
    get: function() { return name; },
    set: function(newName) { name = newName; }
});
cat.name = 'Kitteh';

alert(cat.name); // Kitteh
alert(dog.name); // Kitteh

Every object is going to share the same local variable. If you're looking for a simple approach, we've been using the following for a long time.

function Animal() {
    var name;
    return {
        get: function() { return name; },
        set: function(newName) { name = newName; }
    }
}

var cat = new Animal(),
    dog = new Animal();
cat.name = 'Kitteh';
dog.name = 'Fido';
console.log(cat.name); // Kitteh
console.log(dog.name); // Fido

I'm not sure what the benefits are of having to create a new scope for every change a single property for every object. Plus, you couldn't use the Object.defineProperty in a function to stay DRY because the property is being used in an anonymous scope.

function defineObjectProperty(obj, prop) {
    Object.defineProperty(obj, prop, {
        get: function() { return ?[prop]; },
        set: function(newName) { ?[prop] = newName; }
    });
}
Mitya, at 19/03/'12 16:26, said:
Hi Ryan. Re: reusability when making multiple objects, see the code snippet after the paragraph beginning "Since each property needs its own tracker variable...". The final code example in your post works fine with a few modifications:

function defineObjectProperty(obj, prop) {
       var propVal;
    Object.defineProperty(obj, prop, {
        get: function() { return propVal; },
        set: function(newVal) { propVal = newVal; }
    });
}
var dog = {}, cat = {};
defineObjectProperty(dog, 'name');
defineObjectProperty(cat, 'name');
dog.name = 'Fido';
cat.name = 'Tinkles';
console.log(dog.name+' - '+cat.name); //Fido - Tinkles
Online Life Experience Degree, at 10/05/'12 15:29, said:
I am not what the benefits are of having to create a new scope form every change a single property for every object.
post new comment