| These articles are abridged from my book on learning Apps Script and Office to Apps migration.
Going GAS, from 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 exampleTo 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 codeI 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;
}
}
});
};
}
|