Variable Scope in Modern JavaScript

It often surprises me when speaking to other JavaScript developers how many are unaware of how variable scope works in the language they code in. By scope we are talking about the visibility of variables in our code; or in other words which parts of the code can access and modify a variable. I regularly find people declaring variables with var in the middle of code, not knowing how JavaScript will scope these.

JavaScript has been going through some big changes over the last few years; these include the addition of a couple of interesting new keywords for declaring variables and deal with scope in new ways to what has come before. let and const were added as part of ES6 (a.k.a. ES2015) which has been around for three years now; browser support is good and for everything else there’s Babel for transpiling ES6 into a more widely adopted JavaScript. So now is a good opportunity to look again at how we declare variables and gain some understanding of how they scope.

In this blog post I am going to run through a load of JavaScript examples to show how global, local and block scopes work. We will also look at let and const statements for anyone who is not already familiar with these.

Global Scope

Let’s begin by considering global scope. Globally declared variables can be accessed and modified anywhere in the code (well almost, but we will come to the exceptions later).

Global variables are declared in the code outside of any function definitions, in the top-level scope.

var a = 'Fred Flinstone'; // This is a global variable
function alpha() {
    console.log(a);
}
alpha(); // Outputs 'Fred Flinstone'

In this example, a is a global variable; it is therefore readily available in any function within our code. So here we can output the value of a from the method alpha. When we call alpha the value Fred Flinstone is written to the console.

When declaring global variables in a web browser they are also properties of the global window object. Take a look at this example:-

var b = 'Wilma Flintstone';
window.b = 'Betty Rubble';
console.log(b); // Outputs 'Betty Rubble'

b can be accessed/modified as the property of the window object window.b. Of course it isn’t necessary to assign the new value to b via the window object, this is just to prove a point. We are more likely to write the above as:-

var b = 'Wilma Flintstone';
b = 'Betty Rubble';
console.log(b); // Outputs 'Betty Rubble'

Be careful when using global variables. They can lead to unreadable code that is also difficult to test. I’ve seen many developers come unstuck with global variables trying to discover where the variable’s value is being changed within their codebase causing unexpected bugs. It is far better to pass variables as arguments to functions than rely on globals. Global variables should be used sparingly if at all.

If you really need to use the global scope it is a good idea to namespace your variables so that they become properties of a global object. For example, create a global object named something like globals or app.

var app = {}; // A global object
app.foo = 'Homer';
app.bar = 'Marge';
function beta() {
    console.log(app.bar);
}
beta(); // Outputs 'Marge'

If you are using NodeJS then the top-level scope is not the same as the global scope. If you use var foobar in a NodeJS module it is local to that module. To define a global variable in NodeJS we need to use the global namespace object, global.

global.foobar = 'Hello World!'; // This is a global variable in NodeJS

It’s important to be aware that if you do not declare a variable using one of the keywords var, let or const in your codebase then the variable is given a global scope.

function gamma() {
    c = 'Top Cat';
}
gamma();
console.log(c); // Outputs 'Top Cat'
console.log(window.c); // Outputs 'Top Cat'

It’s a good idea to always initially declare variables with one of the variable keywords. This way we are in control of each variables scope within our code. Hopefully you can see the potential dangers of forgetting to use a keyword like in the above example.

Local Scope

Now we come to the local scope.

var a = 'Daffy Duck'; // a global variable
function delta(b) {
    // b is a locally scoped variable to delta
    console.log(b);
}
function epsilon() {
    // c is declared as a locally scoped variable to epsilon
    var c = 'Bugs Bunny';
    console.log(c);
}
delta(a); // Outputs 'Daffy Duck'
epsilon(); // Outputs 'Bugs Bunny'
console.log(b); // Throws an error as b is undefined in the global scope

Variables declared within a function are scoped locally to that function. In the examples above both variables b and c are local to their respective methods. b is locally scoped to the function delta as it is being declared as an argument of the method, only the value of a is being passed to the function not the variable itself; c is declared as a local variable of epsilon inside the function. However, what if we have the following code; what does this write to the console?

var d = 'Tom';
function zeta() {
	if (d === undefined) {
		var d = 'Jerry';
	}
	console.log(d);
}
zeta();

The answer is 'Jerry' although at first this may not be obvious. This is one of those nasty job interview questions you might get asked if being grilled over your JavaScript abilities. Inside the function zeta we are declaring a new variable d that has a local scope. When using var to declare variables JavaScript initiates the variable at the top of the current scope regardless of where it has been included in the code. So the fact that we have declared d locally within the conditional is irrelevant. Essentially JavaScript is reinterpreting this code as:-

var d = 'Tom';
function zeta() {
	var d;
	if (d === undefined) {
		d = 'Jerry';
	}
	console.log(d);
}
zeta();

This is known as Hoisting. It’s a feature of JavaScript that is well worth being aware of as it can easily create bugs in your code if variables aren’t initiated at the top of a scope. Thankfully we now have let and const that will help avoid these issues going forward. So let’s take a look at how we can use let to block scope variables…

Block Scope

With the arrival of ES6 a few years ago came two new keywords for declaring variables: let and const. Both keywords allow us to scope to a block of code, that’s anything between two curly braces {}.

let

let is seen by many as a replacement to the existing var. However this isn’t strictly true as they scope variables differently. Whereas a var statement allows us to create a locally scoped variable, let scopes a variable to the block. Of course a block can be a function allowing us to use let pretty much as we would have used var previously.

function eta() {
    let a = 'Scooby Doo';
}
eta();

Here a is block scoped to the function eta. We can also scope to conditional blocks and loops. The block scope includes any sub-blocks contained within the top-level block that the variable is defined.

for (let b = 0; b < 5; b++) {
    if (b % 2) {
      console.log(b);
    }
}
console.log(b); // 'ReferenceError: b is not defined'

In this example b is block scoped to the for loop (which includes the conditional block inside it). So it will output the odd numbers 1 and 3 then throw an error because we can’t access b outside the loop that it was scoped to.

What about our strange little function zeta from earlier where we saw JavaScript’s bizarre hoisting affect? If we rewrite the function to use let what happens?

var d = 'Tom';
function zeta() {
	if (d === undefined) {
		let d = 'Jerry';
	}
	console.log(d);
}
zeta();

This time zeta outputs 'Tom' because d is block scoped to the conditional, but does this mean hoisting is not happening here? No, when we use either let or const JavaScript will still hoist the variables to the top of the scope, but whereas with var the hoisted variables are intiated with undefined, let and const variables remain uninitiated, they exist in a temporal dead zone.

Let’s take a look what happens when we attempt to access a block scoped variable before it is initiated.

function theta() {
    console.log(e); // Outputs 'undefined'
    console.log(f); // 'ReferenceError: d is not defined'
    var e = 'Wile E. Coyote';
    let f = 'Road Runner';
}
theta();

So calling theta will output undefined for the locally scoped variable e and throw an error for the block scoped variable f. We cannot use f until after it is initiated, in this case where we set its value as ‘Road Runner’.

There is one other important difference between let and var that we need to mention before moving on. When we use var in the very top level of our code it becomes a global variable and is added to the window object in browsers. With let whilst the variable will become global in the sense that it is block scoped to the entire codebase, it does not become a property of the window object.

var g = 'Pinky';
let h = 'The Brain';
console.log(window.g); // Outputs 'Pinky'
console.log(window.h); // Outputs undefined

const

I previously mentioned const in passing. This keyword was introduced alongside let as part of ES6. In terms of scope it works the same as let.

if (true) {
  const a = 'Count Duckula';
  console.log(a); // Outputs 'Count Duckula'
}
console.log(a); // Outputs 'ReferenceError: a is not defined'

In this example a is scoped to the if statement so can be accessed inside the conditional, but is undefined outside of it.

Unlike let, variables defined by const cannot be changed through re-assignment.

const b = 'Danger Mouse';
b = 'Greenback'; // Throws 'TypeError: Assignment to constant variable'

However, when working with arrays or objects things are a little different. We still can’t re-assign, so the following will fail:-

const c = ['Sylvester', 'Tweety'];
c = ['Tom', 'Jerry']; // Throws 'TypeError: Assignment to constant variable'

But, we can modify a const array or object unless we use Object.freeze() on the variable to make it immutable.

const d = ['Dick Dastardly', 'Muttley'];
d.pop();
d.push('Penelope Pitstop');
Object.freeze(d);
console.log(d); // Outputs ["Dick Dastardly", "Penelope Pitstop"]
d.push('Professor Pat Pending'); // Throws error

Global + Local Scope

We kind of saw this earlier when looking at hoisting, but let’s take a look at what happens when we redeclare a variable in the local scope that already exists globally.

var a = 'Johnny Bravo'; // Global scope
function iota() {
    var a = 'Momma'; // Local scope
    console.log(a); // Outputs 'Momma'
    console.log(window.a); // Outputs 'Johnny Bravo'
}
iota();
console.log(a); // Outputs 'Johnny Bravo'

When we redeclare a global variable in the local scope JavaScript initiates a new local variable. In this example, we have a global variable a, but inside the function iota we create a new local variable a. The new local variable does not modify the global variable, but if we want to access the global value from within the function we’d need to use the global window object.

For me, this code would be much easier to read if we’d used a global namespace for our global variable and rewrite our function to use block scope:-

var globals = {};
globals.a = 'Johnny Bravo'; // Global scope
function iota() {
    let a = 'Momma'; // Local scope
    console.log(a); // Outputs 'Momma'
    console.log(globals.a); // Outputs 'Johnny Bravo'
}
iota();
console.log(globals.a); // Outputs 'Johnny Bravo'

Local + Block Scope

Hopefully by now the following code should behave as you’d expect.

function kappa() {
    var a = 'Him'; // Local scope
    if (true) {
        let a = 'Mojo Jojo'; // Block scope
        console.log(a); // Outputs 'Mojo Jojo'
    }
    console.log(a); // Outputs 'Him'
}
kappa();

Code like this isn’t particularly readable, but the block scoped variable is a new variable accessible only within the block it is defined. Modifying the block variable will have no affect on the locally scoped variable outside of the block. The same will happen if we use let to declare all variants of the variable a, so we can rewrite the example like this:-

function kappa() {
    let a = 'Him';
    if (true) {
        let a = 'Mojo Jojo';
        console.log(a); // Outputs 'Mojo Jojo'
    }
    console.log(a); // Outputs 'Him'
}
kappa();

var, let or const?

I hope that this overview of scope has given you a better understanding of how JavaScript handles variables. Throughout this post I have shown examples using var, let and const statements for declaring variables. With the arrival of ES6 we can now use let and const where we once would have used var and for many this is the approach they are now taking.

Does that mean var is redundant? Well there is no right or wrong answer, but personally I still like to use var for defining global variables at the top-level. However, I use global variables sparingly and like to create a global namespace as discussed earlier for the few cases where these are needed. Otherwise, for any variables whose values will never change I’d use const and for everything else I go for let.

At the end of the day it is up to you how you declare your variables, but hopefully now you have a better comprehension of how they will scope in your code.

Related Content

Published on