JavaScript getters and setters: varying approaches
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:
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.)
Comments (8)
Axel Rauschmayer, at 14/03/'12 16:51, said:
Mitya, at 14/03/'12 17:14, said:
Axel Rauschmayer, at 15/03/'12 10:08, said:
MItya, at 15/03/'12 10:36, said:
Diego Castorina, at 17/03/'12 12:03, said:
Ryan Gasparini, at 19/03/'12 15:53, said:
Mitya, at 19/03/'12 16:26, said:
Online Life Experience Degree, at 10/05/'12 15:29, said: