If you’ve worked through JavaScript recursion primer and More recursion – parents and children you should be pretty comfortable with how recursion works by now. Now we’re going to apply it to create a simple version of jQuery.extend(). If you are not familiar with it, its a useful function that allows you merge multiple objects – usually used for setting default properties.
Let’s say you have this as the default parameters to something like a charts API
var defaultOptions = { width: 20 , height:100, title:"this is the default title", type:"bar" };
and you want to use most of those settings, but make just a couple of changes, with jQuery you can say
var options = jQuery.extend ( { height:200, type:"pie" } , defaultOptions) ;
and you’ll get back an object like this
{ width: 20 , height:200, title:"this is the default title", type:"pie" };
Deeper
So far pretty simple .. but you may need to go deeper into the object if there are properties beyond the first level.
var defaultOptions = { dimensions: { width: 20 , height:100 }, title:"this is the default title", type:"bar" };
Still not too bad, but you’d have to do this. So you can imagine if you continue this out to multiple levels, we have a good candidate for a solution involving recursion
var options = cUseful.extend ( { dimensions: { height:200 } type:"pie" } , defaultOptions) ;
The cUseful library
Many of the snippets in this section of the site are part of the cUseful library. You can find the details below. This extend function uses a couple of functions from that, and will be implemented there too if you want to use it from a library.
The extend function
You use it like this
var options = cUseful.extend ( yourOptions , default1, default2 .... etc);
You can have as many default objects as you like and the result will be the merger of all those defaults applied to your options, very much like jQuery.extend().
So how to do it ?
/** * a little like the jquery.extend() function * the first object is extended by the 2nd and subsequent objects - its always deep * @param {object} ob to be extended * @param {object...} repeated for as many objects as there are * @return {object} the first object extended */ function extend () { // we have a variable number of arguments if (!arguments.length) { // default with no arguments is to return undefined return undefined; } // validate we have all objects var extenders = [],targetOb; for (var i = 0; i < arguments.length; i++) { if (!isObject(arguments[i])) { throw 'extend arguments must be objects'; } if (i ===0 ) { targetOb = arguments[i]; } else { extenders.push (arguments[i]); } } // set defaults from extender objects extenders.forEach(function(d) { recurse(targetOb, d); }); return targetOb; // run do a deep check function recurse(tob,sob) { Object.keys(sob).forEach(function (k) { // if target ob is completely undefined, then copy the whole thing if (isUndefined(tob[k])) { tob[k] = sob[k]; } // if source ob is an object then we need to recurse to find any missing items in the target ob else if (isObject(sob[k])) { recurse (tob[k] , sob[k]); } }); } }
Some tests
function textend() { // expect an empty object var ob = cUseful.extend ({}); Logger.log(ob); // expect ob to look like cUseful.extended var ob = cUseful.extend (ob , {a:true,b:true}); Logger.log(ob); // expect ob to look like cUseful.extended a/b/c/d/e var ob = cUseful.extend (ob , {c:true,d:true}, {e:true}); Logger.log(ob); // none of these should change, but add f var ob = cUseful.extend (ob, {e:false,d:false}, {a:false} , {c:false}, {f:true}); Logger.log(ob); // see that arrays work var ob = cUseful.extend (ob, {a1:[true,2,3]}); Logger.log(ob); // now some recursing - should not cUseful.extend because a is not an object var ob = cUseful.extend (ob , {a:{aa:false}}, {a:{aa:false}}); Logger.log(ob); // should add first one because g is new object var ob = cUseful.extend (ob , {g:{gg:true}}, {g:{gg:false}}); Logger.log(ob); // should add an extra property to g var ob = cUseful.extend (ob , {g:{ggg:true}}, {g:{gg:false}}); Logger.log(ob); // should not add an extra property to g.ggg since that is not an object var ob = cUseful.extend (ob , {g:{ggg:{gggg:false}}}, {g:{gg:false}}); Logger.log(ob); }
and the results
[15-02-08 13:32:34:591 GMT] {} [15-02-08 13:32:34:592 GMT] {b=true, a=true} [15-02-08 13:32:34:593 GMT] {d=true, e=true, b=true, c=true, a=true} [15-02-08 13:32:34:594 GMT] {f=true, d=true, e=true, b=true, c=true, a=true} [15-02-08 13:32:34:595 GMT] {f=true, d=true, e=true, b=true, c=true, a=true, a1=[true, 2.0, 3.0]} [15-02-08 13:32:34:596 GMT] {f=true, d=true, e=true, b=true, c=true, a=true, a1=[true, 2.0, 3.0]} [15-02-08 13:32:34:597 GMT] {f=true, g={gg=true}, d=true, e=true, b=true, c=true, a=true, a1=[true, 2.0, 3.0]} [15-02-08 13:32:34:597 GMT] {f=true, g={gg=true, ggg=true}, d=true, e=true, b=true, c=true, a=true, a1=[true, 2.0, 3.0]} [15-02-08 13:32:34:598 GMT] {f=true, g={gg=true, ggg=true}, d=true, e=true, b=true, c=true, a=true, a1=[true, 2.0, 3.0]}
Let’s check on our initial example.
var defaultOptions = { dimensions: { width: 20 , height:100 }, title:"this is the default title", type:"bar" }; Logger.log(cUseful.extend ( { dimensions: { height:200 }, type:"pie" } , defaultOptions)) ;
and the result
{ "dimensions": { "height": 200, "width": 20 }, "type": "pie", "title": "this is the default title" }
Walkthrough
- Since we have a variable number of arguments, we use the special arguments array like object to pick the up from – returning undefined if we don’t get any.
// we have a variable number of arguments if (!arguments.length) { // default with no arguments is to return undefined return undefined; }
- Pick up each of the objects from the arguments array, check that they are in fact objects, and set up the target from the first one, and note all of the extenders from which to apply their default values.
// validate we have all objects var extenders = [],targetOb; for (var i = 0; i < arguments.length; i++) { if (!isObject(arguments[i])) { throw 'extend arguments must be objects'; } if (i ===0 ) { targetOb = arguments[i]; } else { extenders.push (arguments[i]); } }
- For each of those extenders, apply their defaults. We have to do it recursively as each property of each extender may have sub properties.
// set defaults from extender objects extenders.forEach(function(d) { recurse(targetOb, d); }); return targetOb;
Here’s the recursive function – it calls itself with the properties at the current level if any of them are an object so that matching properties from the default objects can be applied. These are analogous to the children in JavaScript recursion primer
// run do a deep check function recurse(tob,sob) { Object.keys(sob).forEach(function (k) { // if target ob is completely undefined, then copy the whole thing if (isUndefined(tob[k])) { tob[k] = sob[k]; } // if source ob is an object then we need to recurse to find any missing items in the target ob else if (isObject(sob[k])) { recurse (tob[k] , sob[k]); } }); }
And that’s all there is to it. Happy recursing.
Why not join our forum, follow the blog or follow me on Twitter to ensure you get updates when they are available.