Lesson Thursday

When we first introduced variables, we mentioned that there are differences in how var and let/const are scoped. At the time, we weren't ready to discuss those differences. Now that we are actively branching, though, we can explore this difference in a little more detail. It's important because there is another layer of scope that goes beyond local versus global scope.

Let's take a look at a function that includes branching and then illustrate the differences between var and let/const.

function doYouLikeApples(boolean) {
  if (boolean) {
    var string = "Apples are delicious!";
  } else {
    string = "Maybe oranges would be better.";
  }
  return string;
}

In the function above, the value of string is available everywhere in the doYouLikeApples() function. As a result, the code above is yucky and should be refactored to look like this:

function doYouLikeApples(boolean) {
  var string;
  if (boolean) {
    string = "Apples are delicious!";
  } else {
    string = "Maybe oranges would be better.";
  }
  return string;
}

The examples above are exactly the same. With var, all variables are "hoisted" to the outermost level of the function.

Unfortunately, this isn't very useful. Ideally, we should always scope all variables as tightly as possible. What if we have a variable that we only need to use inside a conditional block? It would be better if we just scoped it to the conditional block instead of to the whole function. var doesn't let us do that.

This is one of the problems that let and const fix.

Block Scoping

What happens if we rewrite our first example above to use let or const instead of var? Well, let's take a look. Be warned - we are going to see some tricky behavior.

function doYouLikeApples(boolean) {
  if (boolean) {
    let string = "Apples are delicious!";
  } else {
    string = "Maybe oranges would be better.";
  }
  return string;
}

All we are doing is changing var to let from the first example in this lesson.

So what happens if we call doYouLikeApples(true)?

We'll get the following error:

Uncaught ReferenceError: string is not defined

This is because let and const also use block scoping.

When we scope to a block, the scope remains inside the curly braces of each conditional statement.

In the example above, that means the string variable is scoped inside the curly braces:

if (boolean) {
  let string = "Apples are delicious!";
}

When we try to return string, we are at a higher level of scope than the block where string was defined. For that reason, the variable has fallen out of scope and the function doesn't have access to it.

Now let's do something tricky. What do you think happens when we try calling doYouLikeApples(false)?

It will return "Maybe oranges would be better.". So what happened?

Well, we just created a global variable. Because let and const can be scoped to blocks, when we use string a second time, it's not referring to the variable we declared in the first conditional. Even though it has the same name, it's not in the same scope. Instead, a new variable is created. Because we don't use let, const, or var to declare it, it's a global. We can confirm this in the console by checking the value of string after the function has been called. We'll see that string retains its value even though the variables inside the function should no longer be in scope.

We can still use let and const to have local scope at the outermost level of a function. We just need to do something like this:

function doYouLikeApples(boolean) {
  let string;
  if (boolean) {
    string = "Apples are delicious!";
  } else {
    string = "Maybe oranges would be better.";
  }
  return string;
}

Because string is being declared at the uppermost level of the function, it can be accessed anywhere in the function.

So as we can see, block scoping gives us more granular control over scope, which is a good thing. Since we want to always scope as tightly as possible, if we want to scope to the level of the block, we should have that option. Old school JavaScript with var didn't allow that. let and const do.

Here's one other little behavior that's different between var and let/const. It's a little thing, but it's one more way let and const make JavaScript more consistent and developer-friendly.

function doYouLikeApples(boolean) {
  if (boolean) {
    string = "Apples are delicious!";
  } else {
    string = "Maybe oranges would be better.";
  }
  let string;
  return string;
}

If we call this function, we'll get the following error:

Uncaught ReferenceError: Cannot access 'string' before initialization

This makes sense. We aren't defining string until after we use it. Even if that worked, it would be sloppy code and hard to read.

However, replace the let with var in the function above and everything works just as if string were defined at the beginning of the function. This is because var automatically scopes all variables to the level of the function no matter where they are declared. It's not really a convenience at all - in fact, it's JavaScript being a bit too loosey-goosey. The problem with loosey-goosey code is that it results in annoying bugs.

In this lesson, we covered the difference between block and function scope and a few more reasons why let and const are better than var. Block scoping doesn't just apply to conditionals - it also applies to switch statements and loops. We won't cover looping until next week so don't worry about that yet.

When you write functions that include blocks, always consider whether any variables you declare can be scoped more tightly. There is no need to scope a variable to the top level of the function if it's only needed in a block. Paying close attention to this granularity of scope is a key step you can take towards becoming a better programmer.

Lesson 6 of 11
Last updated September 13, 2020