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.
Key Takeaways
- JavaScript has two main types of variable scope: global and local, with local scope further divided into function scope (for `var`) and block scope (for `let` and `const`).
- Variable declarations using `var` are hoisted to the top of their function scope, while `let` and `const` declarations are hoisted to the top of their block scope but are not initialized, creating a “temporal dead zone.”
- Global variables are accessible throughout the entire program, but their use is generally discouraged due to potential conflicts and overwriting issues.
- Function declarations are hoisted completely, allowing them to be called before they are defined in the code, unlike function expressions which are affected by variable hoisting rules.
- Understanding and correctly utilizing JavaScript’s scoping and hoisting rules can help prevent common bugs related to variable declaration and reduce potential errors in code execution.
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
andconst
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
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.
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.
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.
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.
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.
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.
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 ;)