Extending Objects with JavaScript Getters

Most browsers are coalescing around a consistent API for defining JavaScript Getters and Setters. I’m not entirely comfortable with custom getters and setters – JavaScript’s clean syntax is now a little murkier, and we have a new pitfall to avoid when iterating and cloning object properties, not to mention a significant risk of involuntary recursion – but still I’ll admit they have their uses.

I’m going to publish a more in depth article on getters and setters in a few weeks, in which I’ll document the risks and workarounds more fully, but today I’m going to demonstrate a positive usage – a lightweight utility that uses JavaScript Getters to endow regular Objects with Array-like capabilities. Lets start with a very brief syntax overview:

The Basics

JavaScript Getters and Setters are functions that get invoked when an object’s property is accessed or updated.

var rectangle = {height:20, width:10};

rectangle .__defineGetter__("area", function() {
    return rectangle.height * rectangle.width;  
});
rectangle .__defineSetter__("area", function(val) {
    alert("sorry, you can't update area directly");  
});

rectangle.area; //200
rectangle.area = 150; //alerts "sorry..." etc.
rectangle.area; //still 200

 
There's also an alternative, more declarative syntax that looks prettier but does not allow getters and setters to be assigned dynamically once the object has been created. Moreover I find it less expressive in terms of the JavaScript object model - think function expression vs. function declaration:

var rectangle = {
    height:20, 
    width:10,
    get area() {
        return rectangle.height * rectangle.width;
    },  
    set area(val) {
        alert("sorry, you can't update area directly");
    }  
}

 
ECMA 5 defines a similar syntax for defining getters and setters via the Object.defineProperty function.

var rectangle = {
    width: 20,
    height: 10,
};

Object.defineProperty(rectangle, "area", {
    get: function() {
        return this.width*this.height;
    },
    set: function(val) {
        alert("no no no");
    }
}); 

 
Finally there's a couple of methods you're sure to need. They let us know which properties are represented by getters or setters. They are as fundamental to object recursion as our old friend hasOwnProperty:

rectangle.__lookupGetter__("area"); //area Getter function
rectangle.__lookupSetter__("area"); //area Setter function
rectangle.__lookupGetter__("width"); //undefined
rectangle.__lookupSetter__("width"); //undefined

 
Oh, I should mention this syntax is not supported for IE<9. Ok, now for the fun part:

Use Case: Making Objects work with Array.prototype functions

Much of the ECMAScript 5 API is designed to be generic. If your object supplies certain requisite properties, JavaScript will at least attempt to invoke the function. Most functions defined by Array.prototype are generic. Any regular object that defines properties for the relevant indexes and length gets a crack at the Array API (note that an object is, by definition, unordered so that even if we get to make it work like and array, indexing consistency is not guaranteed)

The brute force approach

First lets see what happens when we try to simply add these properties directly:

//Bad example - apply array properties directly
var myObj = {
    a: 123,
    b: 345,
    c: 546,
}

//iterate properties and assign each value to indexed property
var index = 0;
for (var prop in myObj) {
    myObj[index] = myObj[prop]; 
    index++;
}   
myObj.length = //??????

 
Whoops there's at least two problems here. First we are adding properties even as we iterate, risking an infinite loop. Second we just doubled the number of properties. Does that mean length is now 6? That's not want we wanted at all. The indexed properties should be virtual not physical - they should merely be alternate views over the original properties. A perfect job for...

The Getter approach

This seems more promising. We can easily assign a getter for the array-like properties:

function extendAsArray(obj) {
	var index = 0;
	for (var prop in obj) {
		(function(thisIndex, thisProp) {
			obj.__defineGetter__(thisIndex, function() {return obj[thisProp]});
		})(index, prop)
		index++;
	};
	obj.__defineGetter__("length", function() {return index});
    return obj;
}

 
Lets try it out...

var myObj = {
    a: 123,
    b: 345,
    c: 546,
}

extendAsArray(myObj);

myObj[1]; //345
myObj.length; //3
myObj[2] == myObj.c; //true

 
OK much better - now dare we try a function from Array.prototype?

[].slice.call(myObj,1); //[345, 546] 

 
It worked!, but wait...

re-running the extend function

Our new properties are only accurate so long as our object's state does not change. If we update the object's properties we will need to run our extend function again:

myObj.d = 764;
extendAsArray(myObj);
myObj.length; 8!!??

 
Why did the length suddenly double? Because our function is iterating every property and second time around that includes our shiny new getters. We need to modify the function so that the iteration skips getters. We can do this with the built-in __lookupGetter__ function:

function extendAsArray(obj) {
	var index = 0;
	for (var prop in obj) {
		if(!obj.__lookupGetter__(prop)) {
			(function(thisIndex, thisProp) {
				obj.__defineGetter__(thisIndex, function() {return obj[thisProp]});
			})(index, prop)
			index++;
		}
	};
	obj.__defineGetter__("length", function() {return index});
    return obj;
}

 
objects that already define the length property

Turns out there's still one more problem. What if we try running a function (which is, after all an object) through our extend function?

extendAsArray(alert); //TypeError: redeclaration of const length 

 
Functions (and arrays) are two types of object that already define a length property and they won't take kindly to you trying to redeclare it. In any case we don't want (or need) to extend these types of objects. Moreover some regular objects may also have been initially defined with a length property - we should leave these alone too. In fact the only time its ok for our function to overwrite an existing length property is when that property is a getter.

finally!

Here is our function updated accordingly:

function extendAsArray(obj) {
    if (obj.length === undefined || obj.__lookupGetter__('length')) {
        var index = 0;
        for (var prop in obj) {
            if(!obj.__lookupGetter__(prop)) {
                (function(thisIndex, thisProp) {
                    obj.__defineGetter__(thisIndex, function() {return obj[thisProp]});
                })(index, prop)
            	index++;
            }
        };
        obj.__defineGetter__("length", function() {return index});
    }
    return obj;
}

 
OK, lets put it through its paces...

Practical applications of extendAsArray

 
general showcase

Consider an object that positions and sizes a lightbox, or similar:

var myObj = {
    left:50,
    top:20,
    width:10,
    height:10
}

 
Let's extend this object and subject it to a broad swathe of the array prototype. We'll cache an array instance to cut down on object creation.

extendAsArray(myObj);

var arr = [];
arr.join.call(myObj, ', '); //"50 ,20 ,10, 10" 
arr.slice.call(myObj, 2); [10,10]
arr.map.call(myObj,function(s){return s+' px'}).join(', '); 
//"50px ,20px ,10px, 10px" 
arr.every.call(myObj,function(s){return !(s%10)}); 
//true (all values divisible by 10)
arr.forEach.call(myObj,function(s){window.console && console.log(s)}); 
//(logs all values)

 
By the way, array's toString is also supposed to be generic as of ECMA 5 but does not work generically in any of my browsers.

summarizing numerical data

Now this looks like your latest expense account:

var expenses = {
	hotel: 147.16,
	dinner: 43.00,
	drinks: 15.20,
	taxi: 13.00,
	others: 12.15
}

 
...using extendAsArray we can concisely obtain the biggest expense and also sum the expenses:

extendAsArray(expenses);
var biggestExpense = 
    Math.max.apply(null, [].slice.call(expenses)); //147.16
var totalExpenses = 
    [].reduce.call(expenses, function(t,s){return t+s}); //230.51

 
prototype overview

Prototypes are regular objects too. So, for example, we can easily return an array containing all functions in JQuery's fx prototype:

var fxP = extendAsArray(jQuery.fx.prototype);
//make an array of all functions in jQuery.fx.prototype
[].filter.call(fxP, function(s){
    return typeof s == "function"
}); //(6 functions)

 

what about setters?

It would be handy to also define setters for array's must-have properties. We could automatically update the array-like properties every time state is added, and we would also be able to support array's writeable API (e.g. push, shift etc.). Unfortunately, since it's not possible to anticipate which index properties the user will attempt to update, using current browser implementations we would have to write a setter for every index from 1 to infinity! As I understand it, mozilla has discussed an upcoming feature that would allow object creators to intercept all property updates with a default function - but not sure how far along that got.

etc.

And that about wraps it up. There are hundreds more uses for such array-compliant objects. Those of you familiar with JQuery no doubt already take advantage of a similar construct, but I hope this ultra-compact version serves to demonstrate that for all the headaches, JavaScript Getters may yet bring us a little joy too. More about those headaches, and a more in depth analysis of getters and setters coming in a future article.

Further reading

MDC - Defining Getters and Setters
ECMA-262 5th Edition 15.2.3.6 Object.defineProperty

About these ads

13 thoughts on “Extending Objects with JavaScript Getters

  1. Pingback: Extending Objects with JavaScript Getters | JavaScript, JavaScript – javascript - dowiedz się więcej!

  2. Pingback: HTML all you need to know» Blog Archive » Extending Objects with JavaScript Getters | JavaScript, JavaScript

  3. I don’t really understand JavaScript “Getters” and “Setters”, but I am glad that your excellent weblog is around for reference and resource if my attempts at creating artificial intelligence in JavaScript get a lot more complicated.

    • @Mentifex – thanks for the nice words
      Yeah I wouldn’t necessarily rush into getters and setters – as I hinted up front, the world might even be a better place without them :-)

      • An simple and evil tale in the dark side of the settters and getters:

        Object.prototype.takeGiftFromSauron = function() {
          function makeRingWraith() {
            var me = this;
            Object.getOwnPropertyNames(me).forEach(function(name){
              if (name === 'Object') return;
              var bag = Object.getOwnPropertyDescriptor(me,name);
              if (bag.configurable) delete me[name];
              else if (bag.writable) me[name] = null;
              else alert('damn '+name+'!');
            });
          }
          Object.defineProperty(this,'ring',{get:makeRingWraith});
          return this;
        }
        
        var Frodo = Object.defineProperties({},{
          'coins':{value:3,enumerable:true},
          'soul': {value:'simple',enumerable:true,configurable:true},
          'sword':{value:'elfic',enumerable:true,configurable:true},
          'cape': {value:'brown',enumerable:true,writable:true}
        });
        
        // Frodo do you like this innocent ring?
        Frodo.takeGiftFromSauron();
        // Wear the ring Frodo!!
        alert(Frodo.ring);
        // Surprise! Little avaricious hobbit, where are your soul and your elfic sword?
        
      • This is brilliant Jose :-)

        Perfect illustration of corruption of our innocent language. If I wanted this kind of back room workaround I would code in C++
        And then there is there’s getter/setter infinite recursion problem which is so easy to hit (but that’s for another blog)

  4. Pingback: Tweets that mention Extending Objects with JavaScript Getters | JavaScript, JavaScript -- Topsy.com

  5. Pingback: JavaScript Magazine Blog for JSMag » Blog Archive » News Roundup: IE9 and SunSpider, 20 Things I Learned, New Design Patterns book

  6. Pingback: Using Property Setters and Getters with Ext JS | VinylFox

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s