Lazy Property Access

By on October 19, 2012 3:53 pm

The easiest way to provide access to data from an object is to simply provide it as a property of the object. However, what if you want to give others direct property access, but computing the value of the property is non-negligible, and you want to avoid computing it if it is not used?

This scenario is where ECMAScript 5’s getters are valuable, as they allow you to define a property that is computed on access. We can go a step further and cache the result so it is only computed once at most (even for multiple accesses), while still avoiding computation if it isn’t necessary. This can be accomplished with this function:

var lazyProperty = function(target, propertyName, callback){
	var result, done;
	Object.defineProperty(target, propertyName, {
		get: function(){ // Define the getter
			if(!done){
				// We cache the result and only compute once.
				done = true;
				result = callback.call(target);
			}
			return result;
		},
		// Keep it enumerable and configurable, certainly not necessary.
		enumerable: true,
		configurable: true
	});
}

Then, we could create an object with a property that is only computed if accessed:

obj = {
	first: "John",
	last: "Doe"
}
lazyProperty(obj, "fullName", function(){
	return this.first + " " + this.last;
	// OK, this isn't really that expensive, but we can certainly
	// imagine scenarios where it may be expensive.
});

// This is only computed when the property is actually accessed.
obj.fullName // => "John Doe" 

// Subsequent accesses are cached, and don't need to be recomputed.
obj.fullName // => "John Doe"

This is nice, but unfortunately it only works in ECMAScript 5, thus excluding IE8 and earlier, which most of us still have to support. Fortunately, we can at least still satisfy the API of our object by having a fallback to pre-computed properties when Object.defineProperty is not available. Because the lazy property computation doesn’t affect the behavior or correctness of the code, it is only a performance optimization. We can still support older IE, and simply have a degraded experience for those users. We update our function construction to:

var lazyProperty;
try{
	// Test for a working defineProperty. Unfortunately, we can't
	// just test for the presence of the function, since IE8 has it,
	// but it doesn't work correctly.
	Object.defineProperty({}, 'x', {});

	// If we reach this line, the previous call didn't error, and
	// so we can safely assume that we're in an ES5 environment.
	lazyProperty = function(target, propertyName, callback){
		var result, done;
		Object.defineProperty(target, propertyName, {
			get: function(){ // define the getter
				if(!done){
					// we cache the result and only compute once
					done = true;
					result = callback.call(target);
				}
				return result;
			},
			// keep it enumerable and configurable, certainly not necessary
			enumerable: true,
			configurable: true
		});
	}
} catch(e) {
	// No ES5 defineProperty/getter support, just immediately compute.
	lazyProperty = function(target, propertyName, callback){
		target[propertyName] = callback.call(target);
	};
}

We can now use the lazyProperty function on any browser/VM. We can test the computation timing like this:

var obj = {};

lazyProperty(obj, "foo", function(){
	console.log("calculating foo...");
	return "bar";
});

obj.foo // => "bar"
obj.foo // => "bar"

On all browsers, obj.foo will correctly return "bar". On all modern browsers, the property won’t be computed (and the console.log won’t output) until obj.foo is accessed. On older IE, it will be computed immediately and the console.log will happen immediately. In both cases, the output only happens one time.

Object.defineProperty is a great API that’s available to us with ECMAScript 5. We can start using it right now, while still providing clean fallbacks for older browsers.