New Solutions to Old JavaScript Problems: 1) Variable Scope

Tim Brock / Tuesday, March 22, 2016

Introduction

I love JavaScript but I'm also well aware that, as a programming language, it's far from perfect. Two excellent books, Douglas Crockford's JavaScript : The Good Parts and David Herman's Effective JavaScript, have helped me a lot with understanding and finding workarounds for some of the weirdest behavior. But Crockford's book is now over seven years old, a very long time in the world of web development. ECMAScript 6 (aka ES6, ECMAScript2015 and several other things), the latest JavaScript standard, offers some new features that allow for simpler solutions to these old problems. I intend to illustrate a few of these features, examples of the problems they solve, and their limitations in this and subsequent articles.

Almost all the new solutions covered in this series also involve new JavaScript syntax, not just additional methods that can be pollyfilled. Because of this, if you want to use them today and have your JavaScript code work across a wide range of browsers then your only real option is to use a transpiler like Babel or Traceur to convert your ES6 to valid ES5. If you're already using a task runner like Gulp or Grunt this may not be too big a deal, but if you only want to write a short script to perform a simple task it may be easier to use old syntax and solutions. Browsers are evolving fast and you can check out which browsers support which new features here. If you're just interested in playing around with new features and experimenting to see how they work, all the code used in this series will work in Chrome Canary.

In this first article I am going to look at variable scope and the new let and const keywords.

The Problem with var

Probably one of the most common sources of bugs (at least it's one I trip over on regularly) in JavaScript is due to the fact that variables declared using the var keyword have global scope or function scope and not block scope. To illustrate why this may be a problem, let's first look at a very basic C++ program:

#include <string>
#include <iostream>

using std::cout;
using std::endl;
using std::string;

int main(){
   string myVariable = "global";
   cout << "1) myVariable is " << myVariable << endl;
			
   {
      string myVariable = "local";
      cout << "2) myVariable is " << myVariable << endl;
   }
		
   cout << "3) myVariable is " << myVariable << endl;

   return 0;
}

Compile that program and execute it and it prints out the following:

1) myVariable is global
2) myVariable is local
3) myVariable is global

If you come to JavaScript from C++ (or a similar language that also has block scoping) then you might reasonably expect the code that follows to print out the same message.

var myVariable = "global";
console.log("1) myVariable is " + myVariable);
		
{
   var myVariable = "local";
   console.log("2) myVariable is " + myVariable);
}
	
console.log("3) myVariable is " + myVariable);

What it actually prints out is:

1) myVariable is global
2) myVariable is local
3) myVariable is local

The problem is that re-declaring myVariable inside the braces does nothing to change the scope, which remains global throughout. And this isn't just a problem related to braces that are unattached to any flow-control keywords. For example, you'll get the same result with the following minor change:

var myVariable = "global";
console.log("1) myVariable is " + myVariable);
		
if(true){
   var myVariable = "local";
   console.log("2) myVariable is " + myVariable);
}
	
console.log("3) myVariable is " + myVariable);

The lack of block scoping can also lead to bugs and confusion when implementing callback functions for events. Because the callback functions are not invoked immediately these bugs can be particularly hard to locate. Suppose you have a set of five buttons inside a form.

<form id="my-form">
   <button type="button">Button 1</button>
   <button type="button">Button 2</button>
   <button type="button">Button 3</button>
   <button type="button">Button 4</button>
   <button type="button">Button 5</button>
</form>

You might think the following code would make it so that clicking any of the buttons would bring up an annoying alert dialog box telling you which button number you pressed:

var buttons = document.querySelectorAll("#my-form button");

for(var i=0, n=buttons.length; i<n; i++){
   buttons[i].addEventListener("click", function(evt){
      alert("Hi! I'm button " + (i+1));	
   }, false);
}

In fact, clicking any of the five buttons will bring up an annoying alert dialog box telling you that the button claims to be the mythical button 6.

The issue is that the scope of i is not limited to the (for) block and each callback thinks i has the same value, the value it had when the for loop was terminated.

One solution to this problem is to use an immediately invoked function expression (IIFE) to create a closure, in which the current loop index value is stored, for each iteration of the loop:

for(var i=0, n=buttons.length; i<n; i++){
   (function(index){
      buttons[index].addEventListener("click", function(evt){
         alert("Hi! I'm button " + (index+1));	
      }, false);
   })(i);
}

let and const

ES6 offers a much more elegant solution to the for-loop problem above. Simply swap var for the new let keyword.

for(let i=0, n=buttons.length; i<n; i++){
   buttons[i].addEventListener("click", function(evt){
      alert("Hi! I'm button " + (i+1));	
   }, false);
}

Variables declared using the let keyword are block-scoped and behave much more like variables in languages like C, C++ and Java. Outside of the for loop i doesn't exist, while inside each iteration of the loop there is a fresh binding: the value of i inside each function instance reflects the value from the iteration of the loop in which it was declared, regardless of when it is actually called.

Using let works with the original problem too. The code

let myVariable = "global";
console.log("1) myVariable is " + myVariable);
		
{
   let myVariable = "local";
   console.log("2) myVariable is " + myVariable);
}
	
console.log("3) myVariable is " + myVariable);

does indeed give the output

1) myVariable is global
2) myVariable is local
3) myVariable is global

Alongside let, ES6 also introduces const. Like let, const has block scope but the declaration leads to the creation of a "read-only reference to a value". You can't change the value from 7 to 8 or from "Hello" to "Goodbye" or from a Boolean to an array. Consequently, the following throws a TypeError:

for(const i=0, n=buttons.length; i<n; i++){
   buttons[i].addEventListener("click", function(evt){
      alert("Hi! I'm button " + (i+1));	
   }, false);
}

It's important (and perhaps confusing) to note that declaring an object with the const keyword does not make it immutable. You can still change the data stored in an object or array declared with const, you just can't reassign the identifier to some other entity. If you want an object or array that is immutable you need to use Object.freeze (introduced in ES5).