JavaScript closures - how, where and why



These articles are abridged from my  book on learning Apps Script and 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...
function outer () {
    var outerVar = 1;
    function inner () {
      var innerVar = outerVar;    // inner can see variables in outer function
    }
    var thisDoesntWork = innerVar;   // but outer function cant see inner
    return outerVar;
}

Imagine now, that this function returned a function, which referenced outerVar (which wouldn't normally be visible by a calling function)
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.
  var simple = simpleCat (18);
  Logger.log(simple(7))   // 0
  Logger.log(simple(19)); // 1

Here's the simpleCat function. Notice how it's able to have the 'domain' baked in.
/**
* @param {...var_arg} arguments takes any number of arguments
*/
function simpleCat () {
  
  //convert the arguments to an array after sorting
  var domain_ = Array.prototype.slice.call(arguments);
  
  /**
  * 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 getCategory (value);
  };
}


Let's add some default labels and return them instead of the category
var simpleLabel = simpleLabelCat (18,65);
  Logger.log(simpleLabel(7))   // < 18
  Logger.log(simpleLabel(19)); // >= 18 < 65
  Logger.log(simpleLabel(66)); // >= 65

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 forumfollow the blog or follow me on twitter to ensure you get updates when they are available. 

You want to learn Google Apps Script?

Learning Apps Script, (and transitioning from VBA) are covered comprehensively in my my book, Going Gas - from VBA to Apps script, available All formats are available now from O'Reilly,Amazon and all good bookshops. You can also read a preview on O'Reilly

If you prefer Video style learning I also have two courses available. also published by O'Reilly.
Google Apps Script for Developers and Google Apps Script for Beginners.






Comments