Demystifying JavaScript Variable Scope and Hoisting

Share this article

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.

Hoisting

A JavaScript interpreter performs many operations behind the scenes, and one of them is “hoisting”. If you’re not aware of this “hidden” behavior, it can cause a lot of confusion. The best way of thinking about the behavior of JavaScript variables is to always visualize them as consisting of two parts: a declaration and an initialization/assignment:

var state;             // variable declaration
state = "ready";       // variable assignment

var state = "ready";   // declaration plus assignment

In the above code, we first declare the variable state, and then we assign the value "ready" to it. And in the last line of code, we see that these two steps can be combined. But what you need to bear in mind is that, even though they seem like one statement, in practice the JavaScript engine treats that single statement as two separate statements, just as in the first two lines of the example.

We already know that any variable declared within a scope belongs to that scope. But what we don’t know yet is that, no matter where variables are declared within a particular scope, all variable declarations are moved to the top of their scope (global or local). This is called hoisting, as the variable declarations are hoisted to the top of the scope. Note that hoisting only moves the declaration. Any assignments are left in place. Let’s see an example:

console.log(state);   // output: undefined
var state = "ready";

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

As you can see, when we log the value of state, the output is undefined, because we reference it before the actual assignment. You may have expected a ReferenceError to be thrown, because state is not declared yet. But what you don’t know is that the variable is declared and initialized with the default value undefined behind the scene. Here’s how the code is interpreted by a JavaScript engine:

var state;           // moved to the top
console.log(state);   
state = "ready";     // left in place

It’s important to note that the variable is not physically moved. Hoisting is just a model describing what the JS engine does behind the scenes.

Now, let’s see how hoisting works with let variables:

{
  // Temporal dead one (TDZ) starts at the beginning of the scope
  console.log(state);   // output: "ReferenceError: Cannot access 'state' before initialization
  let state = "ready";  // end of TDZ. TDZ ends at actual variable declaration
}   

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

In this example, the console output is not undefined, but a reference error is thrown. Why? let variables, in contrast to var variables, can’t be read/written until they’ve been fully initialized. They’re fully initialized only where they’re actually declared in the code. So, the let variable declaration is hoisted but not initialized with an undefined value, which is the case with var variables. The section from the beginning of the block to the actual variable declaration is called the Temporal Dead Zone. This is a mechanism that ensures better coding practice, forcing you to declare a variable before you use it. If we move the console statement out of TDZ, we’ll get the expected output: ready.

{
  // Temporal dead one (TDZ) starts at the beginning of the scope
  let state = "ready";  // end of TDZ. TDZ ends at actual variable declaration
  console.log(state);   // output: ready
} 

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

Variables declared with const keyword have the same behavior as let variables.

Functions

Hoisting also affects function declarations. But before we see some examples, let’s first learn the difference between a function declaration and function expression:

function showState() {}          // function declaration
var showState = function() {};   // function expression

The easiest way to distinguish a function declaration from a function expression is to check the position of the word function in the statement. If function is the very first thing in the statement, then it’s a function declaration. Otherwise, it’s a function expression.

Function declarations are hoisted completely. This means that the entire function’s body is moved to the top. This allows you to call a function before it has been declared:

showState();            // output: Ready

function showState() {
  console.log("Ready");
} 

var showState = function() {
  console.log("Idle");
};

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

The reason the preceding code works is that the JavaScript engine moves the declaration of the showState() function, and all its content, to the beginning of the scope. The code is interpreted like this:

function showState() {     // moved to the top (function declaration)
  console.log("Ready");
} 

var showState;            // moved to the top (variable declaration)

showState();  

showState = function() {   // left in place (variable assignment)
  console.log("Idle");
};

As you may have noticed, only the function declaration is hoisted, but the function expression isn’t. When a function is assigned to a variable, the rules are the same as for variable hoisting (only the declaration is moved, while the assignment is left in place).

In the code above, we saw that the function declaration takes precedence over the variable declaration. And in the next example, we’ll see that when we have a function declaration versus a variable assignment, the last takes priority:

var showState = function() {
  console.log("Idle");
};

function showState() {
  console.log("Ready");
} 

showState();            // output: Idle

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

This time, we call the showState() function in the last line of the code, which changes the situation. Now we get the output "Idle". Here’s how it looks when interpreted by the JavaScript engine:

function showState(){        // moved to the top (function declaration)
  console.log("Ready");
} 

var showState;               // moved to the top (variable declaration)

showState = function(){      // left in place (variable assignment)
  console.log("Idle");
};

showState();

Note: arrow functions work identically to function expressions.

Classes

Class declarations are also hoisted in a similar way as variables declared with let statement:

// Using the Person class before declaration
var user = new Person('David', 33); // output: ReferenceError: Cannot access 'Person' before initialization

// Class declaration
class Person {
  constructor(name, age) {
    this.name = name; 
    this.age = age;
  }
}

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

In this example, we can see that using the Person class before declaration produces a reference error similar to that in let variables. To fix this, we must use the Person class after the declaration:

// Class declaration
class Person {
  constructor(name, age) {
    this.name = name; 
    this.age = age;
  }
}

// Using the Person class after declaration
var user = new Person('David', 33);
console.log(user); 

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

Classes can also be created using a class expression, by using var, let or const variable declaration statements:

// Using the Person class
console.log(typeof Person);   // output: undefined

var user = new Person('David', 33); // output: TypeError: Person is not a constructor

// Class declaration using variable statement
var Person = class {
  constructor(name, age) {
    this.name = name; 
    this.age = age;
  }
};

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

In this example, we can see that the Person class is hoisted as a function expression, but it can’t be used because its value is undefined. Again, to fix this we must use the Person class after the declaration:

// Using the Person class
console.log(typeof Person); // output: undefined

// Class declaration using variable statement
var Person = class {
  constructor(name, age) {
    this.name = name; 
    this.age = age;
  }
};

// Using the Person class after declaration
var user = new Person('David', 33);
console.log(user);  

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

Things to Remember

  • var variables are function scoped.
  • let and const variables are block scoped (this includes functions too).
  • All declarations — classes, functions and variables — are hoisted to the top of the containing scope, before any part of your code is executed.
  • Functions are hoisted first, then variables.
  • Function declarations have priority over variable declarations, but not over variable assignments.

FAQs on JavaScript Variable Scope and Hoisting

What is variable scope in JavaScript?

Variable scope refers to the region of code where a variable can be accessed or modified. In JavaScript, variables can have either global or local scope.

What is the difference between global and local scope in JavaScript?

Global scope means a variable is accessible throughout the entire code, while local scope restricts the variable’s accessibility to a specific block or function.

What is block scope?

Block scope is a type of local scope introduced with ES6 using the let and const keywords. Variables declared with let and const are limited to the block (enclosed by curly braces) where they are defined.

How does variable hoisting work in JavaScript?

Variable hoisting is a behavior in JavaScript where variable and function declarations are moved to the top of their containing scope during the compilation phase. However, only the declarations are hoisted, not the initializations.

What is the difference between variable declaration and variable initialization in the context of hoisting?

Variable declaration is hoisted to the top of the scope, while initialization remains in place. This means you can use a variable before it’s declared, but the value will be undefined until the point of initialization.

Does hoisting apply to variables declared with let and const?

Hoisting does occur with variables declared using let and const, but unlike var, they are not initialized to undefined. Instead, they enter the “temporal dead zone” until the point of declaration is reached in the code.

Ivaylo GerchevIvaylo Gerchev
View Author

I am a web developer/designer from Bulgaria. My favorite web technologies include SVG, HTML, CSS, Tailwind, JavaScript, Node, Vue, and React. When I'm not programming the Web, I love to program my own reality ;)

ColinIconstHoistingjavascriptjavascript scopeletscopevariable scopevariables
Share this article
Read Next
Get the freshest news and resources for developers, designers and digital creators in your inbox each week