Demystifying JavaScript Variable Scope and Hoisting

Storing values in variables is a fundamental concept in programming. A variable’s “scope” determines when it is and isn’t available throughout your program. Understanding variable scope in JavaScript is one of the keys to building a solid foundation in the language.

This article will explain how JavaScript’s scoping system works. You’ll learn about the different ways to declare variables, the differences between local scope and global scope, and about something called “hoisting” — a JavaScript quirk that can turn an innocent-looking variable declaration into a subtle bug.

Variable Scope

In JavaScript, the scope of a variable is controlled by the location of the variable declaration, and it defines the part of the program where a particular variable is accessible.

Currently, there are three ways to declare a variable in JavaScript: by using the old var keyword, and by using the new let and const keywords. Prior to ES6, using the var keyword was the only way to declare a variable, but now we can use let and const, which have stricter rules and make the code less error prone. We’ll explore the differences between all three keywords below.

Scoping rules vary from language to language. JavaScript has two scopes: global and local. Local scope has two variations: the old function scope, and the new block scope introduced with ES6. It’s worth noting that function scope is actually a special type of a block scope.

Global Scope

In a script, the outermost scope is the global scope. Any variables declared in this scope become global variables and are accessible from anywhere in the program:

// Global Scope

const name = "Monique";

function sayHi() 
  console.log(`Hi $name`);


sayHi();
// Hi Monique

As this simple example shows, the variable name is global. It’s defined in the global scope, and is accessible throughout the program.

But as handy as this might seem, the use of global variables is discouraged in JavaScript. This is, for example, because they can potentially be overwritten by other scripts, or from elsewhere in your program.

Local Scope

Any variables declared inside a block belong to that particular block and become local variables.

A function in JavaScript defines a scope for variables declared using var, let and const. Any variable declared within that function is only accessible from that function and any nested functions.

A code block (if, for, etc.) defines a scope only for variables declared with the let and const keywords. The var keyword is limited to function scope, meaning that new scope can only be created inside functions.

The let and const keywords have block scope, which creates a new, local scope for any block where they’re declared. You can also define standalone code blocks in JavaScript, and they similarly delimit a scope:


  // standalone block scope

Function and block scopes can be nested. In such a situation, with multiple nested scopes, a variable is accessible within its own scope or from inner scope. But outside of its scope, the variable is inaccessible.

A Simple Example to Help Visualize Scope

To make things clear, let’s use a simple metaphor. Every country in our world has frontiers. Everything inside these frontiers belongs to the country’s scope. In every country there are many cities, and each one of them has its own city’s scope. The countries and cities are just like JavaScript functions or blocks. They have their local scopes. The same is true for the continents. Although they are huge in size, they also can be defined as locales.

On the other hand, the world’s oceans can’t be defined as having local scope, because they actually wrap all local objects — continents, countries, and cities — and thus, their scope is defined as global. Let’s visualize this in the next example:

var locales = 
  europe: function()           // The Europe continent's local scope
    var myFriend = "Monique";

    var france = function()    // France country's local scope
      var paris = function()   // The Paris city's local scope
        console.log(myFriend);  // output: Monique
      ;

      paris();
    ;

    france();
  
;

locales.europe();

See the Pen
Variable Scope: 1
by SitePoint (@SitePoint)
on CodePen.

Here, the myFriend variable is available from the paris function, as it was defined in the france function’s outer scope. If we swap the myFriend variable and the console statement, we’ll get ReferenceError: myFriend is not defined, because we can’t reach the inner scope from the outer scope.

Now that we understand what local and global scopes are, and how they’re created, it’s time to learn how the JavaScript interpreter uses them to find a particular variable.

Back to the given metaphor, let’s say I want to find a friend of mine whose name is Monique. I know that she lives in Paris, so I start my searching from there. When I can’t find her in Paris, I go one level up and expand my searching in all of France. But again, she’s not there. Next, I expand my searching again by going another level up. Finally, I find her in Italy, which in our case is the local scope of Europe.

In the previous example, my friend Monique is represented by the variable myFriend. In the last line we call the europe() function, which calls france(), and finally when the paris() function is called, the searching begins. The JavaScript interpreter works from the currently executing scope and works its way out until it finds the variable in question. If the variable is not found in any scope, an exception is thrown.

This type of lookup is called lexical (static) scope. The static structure of a program determines the variable scope. The scope of a variable is defined by its location within the source code, and nested functions have access to variables declared in their outer scope. No matter where a function is called from, or even how it’s called, its lexical scope depends only on where the function was declared.

Now let’s see how the new block scope works:

function testScope(n) 
  if (true) 
    const greeting = 'Hello';
    let name = n;
    console.log(greeting + " " + name); // output: Hello [name]
  
  console.log(greeting + " " + name); // output: ReferenceError: greeting is not defined


testScope('David');   

See the Pen
Variable Scope: 2
by SitePoint (@SitePoint)
on CodePen.

In this example, we can see that the greeting and name variables declared with const and let are inaccessible outside the if block.

Let’s now replace the const and let with var and see what happens:

function testScope(n) 
  if (true) 
    var greeting = 'Hello';
    var name = n;
    console.log(greeting + " " + name); // output: Hello [name]
  
  console.log(greeting + " " + name); // output: Hello [name]


testScope('David');

See the Pen
Variable Scope: 3
by SitePoint (@SitePoint)
on CodePen.

As you can see, when we use the var keyword the variables are reachable in the entire function scope.

In JavaScript, variables with the same name can be specified at multiple layers of nested scope. In such a situation, local variables gain priority over global variables. If you declare a local variable and a global variable with the same name, the local variable will take precedence when you use it inside a function or block. This type of behavior is called shadowing. Simply put, the inner variable shadows the outer.

That’s the exact mechanism used when a JavaScript interpreter is trying to find a particular variable. It starts at the innermost scope being executed at the time, and continues until the first match is found, no matter whether there are other variables with the same name in the outer levels or not. Let’s see an example:

var test = "I'm global";

function testScope() 
  var test = "I'm local";

  console.log (test);     


testScope();           // output: I'm local

console.log(test);     // output: I'm global

See the Pen
Variable Scope: 4
by SitePoint (@SitePoint)
on CodePen.

Even with the same name, the local variable doesn’t overwrite the global one after the execution of the testScope() function. But this is not always the case. Let’s consider this:

var test = "I'm global";

function testScope() 
  test = "I'm local";

  console.log(test);     


console.log(test);     // output: I'm global

testScope();           // output: I'm local

console.log(test);     // output: I'm local (the global variable is reassigned)

See the Pen
Variable Scope: 5
by SitePoint (@SitePoint)
on CodePen.

This time, the local variable test overwrites the global variable with the same name. When we run the code inside the testScope() function, the global variable is reassigned. If a local variable is assigned without first being declared with the var keyword, it becomes a global variable. To avoid such unwanted behavior, you should always declare your local variables before you use them. Any variable declared with the var keyword inside a function is a local variable. It’s considered best practice to declare your variables.

Note: in strict mode, it’s an error if you assign value to variable without first declaring the variable.

Continue reading
Demystifying JavaScript Variable Scope and Hoisting
on SitePoint.