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.