These articles are abridged from my  book on Office to Apps migration. Going GASfrom VBA to Google Apps Script. Now available directly from O’Reilly or Amazon.

People usually have a lot of trouble understanding closures in JavaScript. In this post I’m instead going to concentrate on implementing an example. By the time you’ve understood the example, you’ll understand closures.

Making categories
Let’s say you want to group ages into categories. A simple way would be to create a function that took these kind of arguments.

var myCategory = makeCategory ( labels, ranges , value);

along with some logic that worked out which range a value belonged to, and from that, which label applied, something like this

var label = makeCategory ( ['under 18','18 and over'],[18], 21)    // '18 an over

But that means each time you call it, you would need to pass over the labels.
Another way would be to make an instance of an object, perhaps with a constructor , and some methods to perform the analysis against the labels stored in the instance.

var category  = new makeCategory ( ['under 18','18 and over'],[18])
var label = category.getLabel (21);

The way I would tackle this is with a closure function.

Closure functions

You probably already know that inner functions can see variables declared in an outer scope – thus…

Imagine now, that this function returned a function, which referenced outerVar (which wouldn’t normally be visible by a calling function)

[sourcecode language=”javascript”] function outer () {
var outerVar = 1;
return function () {
return outerVar;
}
}

we can use that like this.

function testOuter () {
  var myOuter = outer();
  Logger.log(myOuter());    // the answer is 1
}

This characteristic is known as ‘closure’. If you understood this example, then that’s all you need to know about closures – you’ve nailed it. You’ll find that many popular JavaScript libraries such as D3 and Chroma use this very same technique.

Using closures for the example

To get back to our categorization example, instead of using the techniques already described, we can create a function that returns a closure function which has the ranges and labels already baked in, courtesy of the rules of closure.

Here’s a function that uses closure. It doesn’t have labels yet, but returns the category index, like this.

fvar simple = simpleCat (18);
  Logger.log(simple(7))   // 0
  Logger.log(simple(19)); // 1

and here’s the updated function

function simpleLabelCat () {
//convert the arguments to an array after sorting
var domain_ = Array.prototype.slice.call(arguments);
// prepare some default labels
var labels_ = domain_.map (function (d,i,a) {
return (i ? '>= ' a[i-1] ' ' : '' ) '< ' d ; }); // last category labels_.push (domain_.length ? ('>= ' domain_[domain_.length-1]) : 'all');
/**
* gets the category given a domain
* @param {*} value the value to categorize
* @return {number} the index in the domain
*/
function getCategory (value) {
var index = 0;
while (domain_[index] <= value) {
index ;
}
return index;
}
// closure function
return function (value) {
return labels_[getCategory (value)];
};
}

Methods and properties of closure function.

That’s all fine, but now I have two functions – one for getting the index of the category, and another for getting the label. It would be better if the closure function had methods and properties so I could use it for multiple purposes, as in the examples below.

using this test data

var tests = [1,5,7,15, 65,19,21,40,22,90];

getting the label. Remember that categorize actually returns a function, which we can then call with different values in the loop below.

 var cat = categorize (5,18,22,55,65);
  tests.forEach(function(d) {
    Logger.log (d + ' ' + cat(d).label);
  });

That gives this result, using the automatically generated labels

1 < 5
5 >= 5 < 18
7 >= 5 < 18
15 >= 5 < 18
65 >= 65
19 >= 18 < 22
21 >= 18 < 22
40 >= 22 < 55
22 >= 22 < 55
90 >= 65

But Ideally, I’d like to add better labels.

  cat().labels=['kindergarten','school','college' ,'working','matured','retired'];
  tests.forEach(function(d) {
    Logger.log (d + ' ' + cat(d).label);
  });

Giving this result

1 kindergarten
5 school
7 school
15 school
65 retired
19 college
21 college
40 working
22 working
90 retired

But maybe I want to show the category index rather than the label

tests.forEach(function(d) {
    Logger.log (d + ' ' + cat(d).index);
  });

Giving this result

1 0
5 1
7 1
15 1
65 5
19 2
21 2
40 3
22 3
90 5

I can also get the current labels, and current domain

  Logger.log(cat().domain);
  Logger.log(cat().labels);

Which gives this

[5.0, 18.0, 22.0, 55.0, 65.0]
[kindergarten, school, college, working, matured, retired]

Finally, I’d like a default value for the function, so I’ll define a toString() method – so that cat(d) is the same as cat(d).label

  tests.forEach(function(d) {
    Logger.log (d + ' ' + cat(d));
  });

Where to get the code

I found this to be a handy function, so you can find it in my cUseful library, used as in this example

var cat = cUseful.Utils.categorize (5,18,22,55,65);

Here’s the key for the cUseful library, and it’s also on github, or below.

Mcbr-v4SsYKJP7JMohttAZyz3TLx7pV4j

/**
* @param {...var_arg} arguments takes any number of arguments
* @return {function} a closure function
*/
function categorize(var_arg) {
  //convert the arguments to an array after sorting
  var domain_ = Array.prototype.slice.call(arguments);
  // prepare some default labels
  var labels_ = domain_.map (function (d,i,a) {
    return (i ? '>= '   a[i-1]   ' ' : '' )   '< '   d ;
  });
  // last category
  labels_.push (domain_.length ? ('>= '   domain_[domain_.length-1]) : 'all');
  /**
  * gets the category given a domain
  * @param {*} value the value to categorize
  * @return {number} the index in the domain
  */
  function getCategory (value) {
    var index = 0;
    while (domain_[index] <= value) {
      index  ;
    }
    return index;
  }
  // closure function
  return function (value) { 
    return Object.create(null, {
      index:{
        get:function () {
          return getCategory(value);
        }
      },
      label:{
        get:function () {
          return labels_[getCategory(value)];
        }
      },
      labels:{
        get:function () {
          return labels_;
        },
        set:function (newLabels) {
          if (domain_.length !== newLabels.length-1) {
            throw 'labels should be an array of length '   (domain_.length 1);
          }
          labels_ = newLabels;
        }
      },
      domain:{
        get:function () {
          return domain_;
        }
      },
      toString:{
        value:function (){
          return this.label;
        }
      }
    }); 
  };
}

For more like this, see Google Apps Scripts snippets. Why not join our forum, follow the blog or follow me on twitter to ensure you get updates when they are available.
Additional going GAS topics