var, const and let
One of the key things that V8 has sorted out is the scope of variables. Using var to declare variables meant that anything declared within the scope of a function could easily be accidentally overwritten, causing hard to track down errors. ES6 (since it’s commonly known as V8 in Apps Script – I’ll be referring to it as V8 from now on), has added const and let to the variable declaration vocabulary to help prevent these kind of problems.
Legacy Apps Script
Before switching over to V8 (as described in Apps Script v8), let’s look at these snippets
var overwritten inside a block
Don’t do this
function oldVars () { var myVar = 'outside'; Logger.log(myVar); // 'outside' // this is a block for (var i = 0; i < 2 ; i++) { Logger.log(myVar); // 'outside' // 0 var myVar = i; // <--- D } Logger.log(myVar); // 1 <--- E }
myVar is declared outside the scope of the for loop block, and retains its value inside the loop. However, inadvertently assigning a value at (D) – even by redclaring it – overwrites the original external value. At (E) myVar has now been corrupted by things that went on inside the loop. There may be some reason to want to do this, but it’s very definitely a dangerous anti-pattern.
Const not behaving properly
A const in JavaScript is a value that cannot be reassigned a value within the same block. Another variable of the same name might exist elsewhere in the function, but as long as it is in a separate block, it’s not the same variable. Legacy Apps Script supported the const syntax, but didn’t always behave properly. Here’s a misleading syntax errors reported by legacy apps script
function oldConstVar () { const myVar = 'outside'; // <-- A for (var i = 0; i < 2 ; i++) { const myVar = i; // <--- D TypeError: redeclaration of const myVar } }
Worse still, here’s const behaving improperly, and not reporting it.
function oldConst () { const myVar = 'outside'; // <-- A Logger.log(myVar); // 'outside' // this is a block for (var i = 0; i < 2 ; i++) { myVar = i; // 'outside' <--- D this should have been illegal, but it just ignored it (TypeError: Assignment to constant variable) Logger.log(myVar) } Logger.log(myVar); // 'outside' <--- E Logger.log(i); // 2 <--- F this leaks var values outside the block - ideally this should be undefined }
In this case, it happily accepted a reassignment of myVar without complaint. This would have been the correct behavior if it had redclared myVar inside the block as const myVar, and it did correctly preserve the outside myVar value outside the block. Furthermore, this example shows that the i variable controlling the for loop preserves its value (because it’s a var), but ideally it should be undefined at (F)
V8 scopes
Switching over to V8, we can compare the behavior and new capabilities.
Const redclaration
This one didn’t complain in legacy, but now it correctly notices that there’s an attempt to reassign a value to a variable that’s been declared as a const. We still have that leaking variable i at the end of the for loop though, because var has been used to declare it in the loop.
function v8Const () { const myVar = 'outside'; // <-- A Logger.log(myVar); // 'outside' for (var i = 0; i < 2 ; i++) { myVar = i; // now we correctly get TypeError: Assignment to constant variable Logger.log(myVar) } Logger.log(myVar); // 'outside' <--- E Logger.log(i); // 2 <--- F this leaks var values outside the block - ideally this should be undefined }
Use let instead of var for control loops
If you do need to write a for loop (I don’t remember the last time I did, to be honest though, since array functions are a much better way to iterate and they were available in legacy Apps Script too), then it’s best to use let rather than var to prevent leakage. Here, i is undefined outside the block and only lives as long as the block does
function v8UseLet () { for (let i = 0; i <2 ; i++) { // <-- B let instead of var Logger.log(i) // 0/1 } Logger.log(i); // 2 <--- F since we used let instead of var - ReferenceError: i is not defined correctly detected }
Closures
When we discuss closures we generally mean the way that a function can be created to encapsulated its ‘lexical state’, meaning the variables it can see when it’s declared. I cover that in a number of articles such as JavaScript closures: how, where and why, Abstracting services with closures, JavaScript currying and functional programming and many others but the concept of scope and what it means for closures are very closely aligned. Moving away from var in favour of (mainly) const and (less often) let will help you understand closures more easily.
Inside can see outside const
function v8Closure () { const myVar = 'outside' // <-- A if (myVar === 'outside') { // <-- B it is const myVar = 'inside'; // <-- C this myVar is a differnt myVar if(myVar === 'outside') { // <-- D because it refers to the innermost myVar Logger.log('its not') } else { Logger.log(myVar) // <-- E 'inside' } } Logger.log(myVar) // 'outside' <--E now we're outside the block }
Above, const myVar is declared both inside and outside an if block. In fact, these variables are unrelated. The ‘inside’ myVar is visible only in the (if) block it’s declared in, whereas the ‘outside’ myVar is visible in every block inside the (function) block it’s declared in. This property is the basis of closure.
Inside can see outside let
let works in the same way
function v8Let () { let myVar = 'outside' // <-- A if (myVar === 'outside') { // <-- B it is let myVar = 'inside'; // <-- C this myVar is a differnt myVar if(myVar === 'outside') { // <-- D because it refers to the innermost myVar Logger.log('its not') } else { Logger.log(myVar) // <-- E 'inside' } } Logger.log(myVar) // 'outside' <--E now we're outside the block }
declaration versus reassignment
However, in the case below, there is only one myVar, and the inner block is modifying it. That’s because we didn’t redeclare it at (C), just simply assigned a different value to the myVar already declared at (A)
function v8LetMore () { let myVar = 'outside' // <-- A if (myVar === 'outside') { // <-- B it is myVar = 'inside'; // <-- C this myVar is the same myVar if(myVar === 'outside') { // <-- D it still refers to the outer myvar Logger.log('its still not') } else { Logger.log(myVar) // <-- E 'inside' } } Logger.log(myVar) // 'inside' <--E because we updated the same myVar each time }
Applying this to closures
Here’s an example of a simple closure
function testAdd () { Logger.log(addSmith()('john')); // john smith } function addSmith (){ const lastName = 'smith'; return function (firstName) { return firstName + ' ' + lastName; }; }
The function addSmith returns not a result, but another function that’s expecting an argument of firstName. So where does the lastName come from? Since the function returned by addSmith has lastName defined at a higher level it ‘inherits’ the context it was defined in, and therefore knows the value of lastName.
Let’s take it a little further, parameterizing the last name for the closure
function testAdd () { Logger.log(addName('Smith')('john')); // john smith } function addName (lastName) { return function (firstName) { return firstName + ' ' + lastName; }; }
And a little further
function testAdd () { const flintstones = addName('flintstone'); const rubbles = addName('rubble'); Logger.log([ flintstones('fred'), flintstones('wilma'), flintstones('pebbles'), flintstones('dino'), rubbles('barney'), rubbles('betty'), rubbles('bambam') ].join(",")); // fred flintstone,wilma flintstone,pebbles flintstone,dino flintstone, // barney rubble,betty rubble,bambam rubble } function addName (lastName) { return function (firstName) { return firstName + ' ' + lastName; }; }
Of course, this is a fairly daft example, but the idea of closures, and from that ‘currying’ allow you to build a reuse complex workflows with simple, reusable building blocks.
Golden rules
So we’ve seen these 3 things
- var can be reassigned to and is the same var within a complete function. (note that the global scope is itself a function, so variables declared in the global scope are visible inside all functions)
- const can be used to have many variables of the same name, but in different blocks (function/if/for/switch etc – essentially section of code that is typically enclosed within {…curly brackets…} is a block) . You can’t reassign a value to a const in the same block scope.
- let is the same as const, but allows reassignment
Here are the rules that I tend to follow, but they are my rules, so they might not work for you.
- never use var
- nearly always use const
- use let if an inner block is being allowed to reassign to a variable at a higher scope
- always declare a variable at the innermost scope it is needed at
- Avoid loops – use array functions, or sometimes, recursion
- Avoid switch – it’s a mess
- Never put anything in the global scope, other than namespaces (Scope and Namespaces)
All these examples still use the function () {} model, but there are better ways to do that in V8, which I’ll address in another article