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 whyAbstracting services with closuresJavaScript 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