Coming soon to JavaScript: block scope with let | WickedlySmart

In my previous post, I experimented with the new const keyword which you can use to create constants in JavaScript. Along with var and const, we’ll soon have a third way to declare values in JavaScript: the let keyword.

Like var, you’ll use let to create variables (not constants). You might be wondering why we need two ways to create variables. Well, let differs from var in one very important way: let is scoped differently from var. Let’s take a closer look at what that means.

Remember that as of ECMAScript 5, JavaScript has only two kinds of scope: global scope and function scope. That means a variable is either a global variable, visible everywhere in your code, or it’s a variable local to a function, visible everywhere in that function. This is mostly okay but has a subtle consequence: you can refer to variable before it’s defined without getting a reference error.

For instance, in the following code:

function pets() {
    var dog = "Fido";
    console.log("cat is ", cat);
    var cat = "Fluffy";
}

When you call pets() you’ll see:

> pets()
cat is  undefined

That means that the cat variable exists in the line above where you declare it! We know it exists because we don’t see a reference error.

Side note: to see a reference error, simply change the variable cat to zebra in the line where we are calling console.log:

function pets() {
    var dog = "Fido";
    console.log("cat is ", zebra);
    var cat = "Fluffy";
}

> pets()
Uncaught ReferenceError: zebra is not defined

In this example, the variable zebra really doesn’t exist at all, so we see the reference error.

What happens with var is something called hoisting. When you write code like our pets function above, the cat variable is hoisted, so it’s as if you wrote pets like this:

function pets() {
    var dog = "Fido";
    var cat;
    console.log("cat is ", cat);
    cat = "Fluffy";
}

Notice that the declaration of cat is moved up above where we’re using cat. It’s not being initialized, which is why we see undefined as the value for cat when we display it in the console, but because the declaration is above where we use it, we don’t get a reference error.

This hoisting happens even when you declare a variable within a block in a function, like this:

function pets() {
    var dog = "Fido";
    console.log("cat is ", cat);
    if (true) {
        var cat = "Fluffy";
    }
}

Call pets, and you’ll see exactly what we saw before, that cat is undefined. Weirder still, you’ll find that cat is declared even if the code where you declare it never gets executed at all:

function pets() {
    var dog = "Fido";
    console.log("cat is ", cat);
    if (false) {
        var cat = "Fluffy";
    }
}

Notice here that even though the if block is never executed, when we display the value of cat in the console, we get undefined and not a reference error.

This all happens because the scope of cat is the entire pets function. If you imagine a scope being like a rectangle you draw around the code that defines the scope, the scope of the variable cat looks like this:

function scope with var

That means that cat is declared and exists everywhere in function pets. Its value won’t be defined until you explicitly give it a value, but the variable exists. (Remember the difference between a declared variable and a defined variable: a declared variable has been declared with the var keyword, and a defined variable has been given a value other than undefined.)

let changes all this. With let, your variables are no longer declared before the flow of execution gets to the statement that declares them. So if you replace var with let in the first version of pets:

function pets() {
    var dog = "Fido";
    console.log("cat is ", cat);
    let cat = "Fluffy";
}

When you call pets(), now you’ll see (Firefox 36.0.4):

> pets()
ReferenceError: can't access lexical declaration 'cat' 
                before initialization

In other words, the variable cat is not visible above where it is declared, like it was when we declared it with var. This behavior is a bit more intuitive (it’s weird to think of variables being declared above the statements that declare them), and it’s a lot easier to understand for programmers coming to JavaScript from other languages where this kind of scoping is the norm.

let also means we now get block scoping, too. With var, as you’ve seen, if a variable is declared inside block nested within a function, the scope of that variable is the entire function, even though you might more intuitively think that the scope of the variable should be limited to the block. That means you can access the value of cat after the block in which it’s declared and defined has completed executing:

function pets() {
    var dog = "Fido";
    if (true) {
        var cat = "Fluffy";
    }
    console.log("cat is ", cat);
}

Call pets and you’ll see:

> pets()
cat is  Fluffy

Again, this is because var has function scope, not block scope; a bit weird for anyone used to a language like, say, Java.

With let, the scope of the variable is limited to the block in which the variable is declared:

function pets() {
    var dog = "Fido";
    if (true) {
        let cat = "Fluffy";
        console.log("cat is (block):", cat);
    }
    console.log("cat is (function):", cat);
}

Call pets and you’ll see:

> pets()
cat is (block): Fluffy
ReferenceError: cat is not defined

Now, we cannot access the variable cat outside the block in which it’s declared; when we try, we get a reference error.

You can visualize the scope of cat declared with let like this:

block scope with let

The scope of cat in this example is a box drawn around the if block, and this scope is nested inside the scope created by the pets function, which is the scope of the variable dog. (Finally, Fido gets a mention!)

So, two important things to remember about let so far are:

  • When you declare a variable with let, it does not exist until the line of code where you declare the variable is executed.
  • When you declare a variable with let inside a nested block, that variable has block scope: it is not visible or accessible outside the block in which it is declared.

The way let works is a pretty fundamental change about the way variables work in JavaScript (although note that var is not going away! We don’t want to break the web…). Now, you might think it’s not that big of a deal, but let has quite a few implications, from the more obvious (the use cases we’ve looked at so far) to the more subtle.

For instance, let’s take a look at using let with a for loop:

function pets(dogs, cats) {
    for (var i = 0; i < dogs.length; i++) {
        console.log("Dog:", dogs[i]);
    }
    console.log("i is:", i);

    for (let j = 0; j < cats.length; j++) {
        console.log("Cat:", cats[j]);
    }
    console.log("j is:", j);
}

In the first for loop, we’re declaring a variable i to use as the loop variable. But as you might guess, the scope of i is the entire function, not just the for loop because we’re declaring it with var. That means i is accessible after the for loop block has completed.

In the second for loop, we’re declaring a variable j to use as the loop variable, but this time we’re using let, so the scope of j is limited to the for loop itself. Let’s test that out:

> pets(["Fido", "Spot"], ["Fluffy", "Pickles"]);
Dog: Fido
Dog: Spot
i is: 2
Cat: Fluffy
Cat: Pickles
ReferenceError: j is not defined

So where we attempt to display the value of j after the loop block has completed, we get a reference error.

Using var or let in a short loop block like this doesn’t make much of a difference most of the time (unless you’re counting on using a loop variable, like i outside of the loop block, which would be a bad idea in general), but let’s take a look at an example where this makes a big difference:

window.onload = function() {
    var button = document.getElementById("button");
    button.onclick = function() {
        var body = document.querySelector("body");
        for (var i = 0; i < 3; i++) {
            var div = document.createElement("div");
            div.id = "div" + i;
            div.innerHTML = "This is div " + i;
            div.onclick = function() {
                console.log("You just clicked div " + i);
            };
            body.appendChild(div);
        }
    };
};

(If you’d like to try this code, you can download it from github along with the HTML and load it into your browser.)

This version of the code uses var. When you run this code, you’ll see a web page with a button “click me”. When you click the button, you’ll see three <div> elements appear:

three divs appear when you click the button

Now try clicking on each <div> element. You will see the same message in the console each time:

You just clicked div 3

I’ll quickly walk through the code, and then we’ll change the var to let so you can see the difference:

code walkthrough

First, we set up a click handler for the <button> in the page with the id “button”. We then use a for loop to create three new <div> elements, and add each to the page by appending it to the <body> element, which just adds each new <div> to the bottom of the page. We give each <div> an id based on the string “div” and the current value of i, so the first time through the loop, the new <div> we create has the id “div0”; the second time through the loop the new <div> we create has the id “div1” and so on. We also use the value of i to set the text of the <div> which you can see in each <div> on the page. All that works fine.

We also set up a click handler for each <div> element, so that when you click on a <div> it should display “You just clicked div ” and then a number depending on which <div> you clicked on. So if you click on the <div> with the id “div0”, you would expect to see “You just clicked on div 0” in the console; for “div1”, you’d expect to see “You just clicked on div 1”, and so on.

If you remember closures from Chapter 11 of Head First JavaScript Programming, you might remember that when you create a click handler for an element object that contains a free variable, you create a closure. A closure is a function with an environment containing the value of any free variable in the function; that is, a variable that’s not defined in the function itself, but rather in the scope surrounding the function when that function is created. So if you look at this example, you can see that each time through the loop, we’re creating a function that is the click handler for the <div> object that we’re creating in that loop, and that function has a free variable, i. i is the loop variable and it’s defined in the scope surrounding the function when we create it. That means that each click handler is actually a closure: a function plus an environment, with the environment containing the value of i. So, later, when you click on one of the <div>s in the page, and invoke the click handler, the click handler function will find a value for i in the environment that is part of its closure.

So, why doesn’t this work? Why do we see “You just clicked on div 3” no matter which <div> we click on? The problem is that each closure has a reference to an environment, and because i has function scope, the environment for all three of the closures is the same! The environment is the function scope. Here’s what that looks like:

closures for var i

Each of the pink boxes is a click handler function with a reference to an environment shown as the purple box. The environment is one environment that corresponds with the scope of the function, because that is the scope of the variable i. Each time through the loop, the value of i changes, and it changes in the environment too. So when the loop completes and the onload function is done, the value of i in the environment that all three closures reference is 3. When you click on any of the <div> elements in the page, you’ll invoke that element’s click handler, which references the value of i. Since i was a free variable when the function was created, the value of i is looked up in the environment for that click handler (the same environment for all three click handlers!) and its value is 3. So all you ever see is “You just clicked on div 3.”

Now let’s look at what happens if we change from var to let when we declare i:

window.onload = function() {
    var button = document.getElementById("button");
    button.onclick = function() {
        var body = document.querySelector("body");
        for (let i = 0; i < 3; i++) {
            var div = document.createElement("div");
            div.id = "div" + i;
            div.innerHTML = "This is div " + i;
            div.onclick = function() {
                console.log("You just clicked div " + i);
            };
            body.appendChild(div);
        }
    };
};

Make this change and reload the page and click on the button. The page will look exactly the same: you’ll see three <div> elements appear, with ids “div0”, “div1”, and “div2”. But, now click on each of the <div>s and you should see three different messages in the console:

You just clicked div 0
You just clicked div 1
You just clicked div 2

(To try this yourself get the code from github).

Now, the code is working perfectly! One small change makes all the difference in the world… So how does it work?

The trick is that because let creates block scope in the for loop, each time we go through the loop a new scope is created. That means, each time you create a new closure with the click handler for each new <div> object, a different environment for the closure is created, and each environment has a different value for i:

closures for let i

So, when you click on a <div> element in the page, the value of the free variable i is looked up in the closure, and this time, it’s correct. It’s correct because each closure captures the individual block scope that was created for that one time through the loop.

Now, sometimes you might want the environment for the closures you create in a function to be shared, like when we used var to declare i, but sometimes you’ll want each closure to have a separate environment, like when we used let to declare i. Now you know how var and let work, and how closures work, you’ll be able to make an informed choice! And, just to link back to the previous post again, note that const is scoped the same way as let.

Keep in mind that let is not fully supported in browsers yet (as of this writing). Keep your eye on the ECMAScript 6 compatibility table for browser support in the future, and in the mean time you can use a transpiler, like Traceur to translate your code that uses let to ECMAScript 5 code that simulates let (and does so remarkably well!). To use Traceur, all you have to do is link to it in the <head> of your document. Check out the code for the let example on github to see how to do this.

Resources

Give your Brain a Treat!

Don't miss out on brain-friendly updates, new WickedlySmart Projects, early access to books and general cool stuff! Just give us your email and we'll send you something about once a week. Don't worry, we'll never sell your name and you can remove yourself at any time.

Check your email to confirm your subscription.

Pin It on Pinterest

Share This