These articles are abridged from my book on 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…
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
- A functional approach to fiddling with sheet data
- A functional approach to updating master sheet
- A recursive extend function for Apps Script
- A webapp to share copies of the contents of folders
- Abstracting services with closures
- Add-on spinner
- Addressing namespace and library methods from google.script.run
- Anonymous user registration with the Apps Script PropertiesService
- Apps for Office – binding example comparison
- Apps Script as a proxy
- Apps Script const scoping problems
- Calculating image dimensions in server side apps script
- Calculating the last day of a given weekday in the month
- Canvasser
- Chaining JavaScript
- Changing class properties dynamically
- Checking the argument types in Apps Script
- Cleaning up a document format
- Cleaning up heading levels
- Column numbers to characters
- Composing functions and functional programming
- Configurable canvas meter
- Convert JSON to XML
- Converting SVG to PNG with JavaScript
- Converting timestamps to dates formula
- Copying canvas and svg images from your Add-on
- Copying to new host location
- Copying to new host location
- Counting script and library usage
- Create sha1 signatures with apps script
- Creating a key digest to use for a cache key or to compare content
- Creating a pile of files list from Google Drive
- Creating and working with transposed sheet data arrays
- Cross Origin Resource sharing (CORS)
- CryptoJS libraries for Google Apps Script
- Custom checking for exponential backoff
- Data wrangling with named columns in Google Spreadsheet
- Dealing with objects that are too large for the property or cache store
- Detecting Spreadsheet tables automatically with Google Apps Script
- Direction minimizer – other usages
- Do something useful with GAS in 5 minutes
- Dynamically creating tables with clusterize.js
- EasyCron Library
- ES6 JavaScript features
- Exponential backoff
- Exponential backoff for promises
- Fiddler and rangeLists
- Fiddling with text fields that look like dates
- Filling ranges in Google Sheets
- Finding a Drive App folder by path
- Finding where Drive hosting is being used in Sites
- Flattening an object with dot syntax
- Flattening and unflattening objects to spreadsheets
- Formatting GraphQL queries
- Formatting sheet column data with fiddler
- From notes to frequencies and back again
- From Xml to JSON
- Generating and managing random lists with JavaScript and Apps Script
- Generating coupon codes with expiry dates
- Generating test data for sheets and tables
- Get GAS library info
- Getting an htmlservice template from a library
- Getting insights into Sheets performance
- Google Drive as cache
- Header formatting with fiddler
- Highlight duplicate rows in a sheet – map and reduce
- Highlight duplicate rows in a sheet – map, filter and every
- How to determine what kind of object something is in Apps Script
- How to get stats about youtube videos in your channel with apps script
- How to pass non stringifyable objects to html service
- How to transpose spreadsheet data with apps script
- Identifying duplicates on Drive
- Identifying hosted files
- Implementing a client side progress bar reporting on server side work for htmlService
- Importing Predictwise data
- Improved namespace pattern for Apps Script
- Including the stack in custom errors
- JavaScript currying and functional programming
- JavaScript currying and functional programming – even more
- JavaScript recursion primer
- JSONP and JSON and Google Apps Script webapps
- Loading large JSON datasets into BigQuery with Apps Script
- Logging differences in strings in Apps Script
- Measuring library load speed
- Migrating user and script properties
- Minimizing maps directionfinder api calls
- More client server code sharing
- More recursion – parents and children
- More sheet data fiddling
- Multiple inserts in Fusion Tables
- Namespaces in libraries and scripts
- Normalizing the header level of blank paragraphs
- Optimizing sheet formatting with Apps Script
- Optimizing showing and hiding rows and columns
- Organizing asynchronous calls to google.script.run
- Organizing parallel streams of server calls with google.script.run promises
- Parallel process orchestration with HtmlService
- Passing data to html service
- Patching site html
- Populating sheets with API data using a Fiddler
- Proxy jsonp
- Pseudo binding in HTML service
- Recursive async functions
- Removing duplicate paragraphs
- Reporting file, function and line number in Apps Script
- Resumable uploads – writing large files to Drive with Apps Script
- Reusing html stuff between Apps Script projects
- Roughly matching text
- Serving apps script to JavaScript app
- Sharing code between client and server
- Shortcut for adding nested properties to a JavaScript object
- Simple server side polling
- Sorting Google Sheet DisplayValues
- Squeezing more into (and getting more out of) Cache services
- Styling Gmail html tables
- Summarizing emails to a sheet
- SunCalc
- Transform dates for add-on transfer
- Transposing sheet data
- Traversing a tree
- Unique values with data fiddler
- Unnesting data to sheet values
- Untangling with promises
- Use Drive properties to find app files
- Use promise instead of callback for settimeout
- Using Advanced Drive service to convert files
- Using Apps Script for xml json conversion
- Using array formulas to improve performance
- Using crossfilter with Google Apps Script
- Using D3 in server side Gas
- Using es6 promises server side in Apps Script
- Using Es6 with Apps Script
- Using exponential backoff with github api – dealing with data “in preparation”
- Using Google sheets via Bigquery from Apps Script
- Using named locks with Google Apps Scripts
- Using promises to orchestrate Html service polling
- Using promises with apps script
- Using the Itunes API with Apps Script
- Using the slideshare API from Apps Script
- Using timing functions to get insight into Sheets
- Watching docs for changes
- Watching for server side changes from the client html service
- What JavaScript engine is Apps Script running on?
- Why Base64
- Zipping to make stuff fit in cache or properties service.