JavaScript Primer: Unlocking Scope
Connecting variable declarations and variable scope in JS
Transforming ideas from imagination into reality. In a broad sense, that is the main goal of programming. A programming language is a tremendously powerful tool for building things and expressing ideas. As with any other tool, skillful use requires a lot of practice and a deep understanding of how the tool works. A programmer who masters the fundamentals of a language is able to leverage the language’s strengths, circumvent its weaknesses, and find enjoyment in its quirks. Mastery of fundamentals is especially valuable for a language like JavaScript — one of the quirkier languages in the modern tech stack — having numerous strengths and a few head-scratching weaknesses.
I have the privilege to be learning JavaScript as part of my studies at Launch School. My goal with this series of articles is to introduce a few of the language’s core topics in a way that is clear, ordered, and concise. This article will cover variables and scope. The articles to follow will dive into hoisting and closures — two frequently misunderstood features of the language. Developing a strong grasp of these topics is a worth-while investment that will pay off down the road.
Declaring Variables
Take a look at the following code:
let
is the preferred keyword for declaring variables. It causes the computer to set aside a location in memory, and label it with the identifier that follows let
, in this case name
. This whole process constitutes variable declaration. Since name
is not given a value here, JavaScript gives it the value undefined
by default.
Let’s start over:
Now in addition to declaring a variable called name
, we give it an initial value of 'Charlie'
. Any expression following the =
in a variable declaration is called an initializer. The expression will be evaluated, and the value it returns will be stored in the variable. Note that this is not the same thing as re-assignment, as re-assignments are stand-alone expressions. Let’s take a look at one.
Now that name
has been declared in memory, we don’t need the let
keyword when giving it a value. Also note that we don’t need to worry about re-assigning a variable to a value with a different data type. Although name
was initialized to a string, we are able to seamlessly re-assign it to a number. This is because JavaScript is dynamically typed.
In addition to let
, the const
keyword can be used to declare variables. The only difference between the two is that variables declared using const
must be initialized to a value, and cannot be re-assigned after that.
Block Scope
In JavaScript, a block is a set of statements and expressions between a pair of opening and closing curly braces. All variables that are declared using let
or const
are said to have block scope. What this means is that variables declared outside of a block can be referenced inside of a block. However, variables that are declared inside of a block cannot be accessed outside of the block. An example will make this more clear:
On line 1, we declare a variable dog
and initialize it to 'Charlie'
. Then on lines 3–6 we have an if
statement with a block. Since variables declared with let
have block scope, dog
can be accessed inside the block. In this code, we choose to re-assign dog
to 'Ruby'
. On the next line, we declare a new variable cat
and give it an initial value of 'Scratch'
. Since this variable is declared inside of the block, it cannot be accessed outside of the block. We can demonstrate this by adding two more lines to the bottom of the code snippet:
In this code, we pass the value of each variable as an argument to console.log()
, which prints the values to the console. When line 8 executes, "Charlie”
is displayed on the console. However, when the next line runs, JavaScript throws a ReferenceError
, saying that the variable cat
is not defined. Even though the variable cat
exists, it is not available to be referenced outside of the block where it was declared.
While we are on the topic of block scope, there is an important nuance to be aware of. The curly braces which wrap around the body of a function are not technically a block. However, they behave just like a block when it comes to variables that have block scope.
Function Scope
let
and const
are actually relatively modern ways to declare variables. They were introduced to JavaScript in 2015 as part of ES6, which added many new features to the language. The traditional way to declare variables is with the var
keyword. There are a handful of differences between variables declared with var
and those declared using let
and const
. We will discuss one of them here and some others in the article about hoisting.
Variables declared using var
have function scope. This means that they are not affected by blocks when it comes to scope, and are only influenced by function bodies. Lets take a look at an example:
We see on line 3 that even though the variable name
is declared inside a block, it is available to be referenced later in the function on line 6 when its value is passed as an argument to console.log()
. However, an error is raised on the last line of code when we try to use name
outside of the function.
In addition to variables declared with var
, function declarations also create variables with function scope. Let’s take a look:
Here we have a function called sayHello
that is declared inside of the function saySomething
. That declaration is actually declaring a variable called sayHello
, and initializing it to the sayHello
function. As we would expect, we can invoke sayHello()
by referencing it anywhere inside of the saySomething
function. However, if we try to call sayHello()
outside of the function body of saySomething
, JavaScript won’t let us.
Local vs. Global
The keyword that is used to declare a variable is only one of the factors that can be considered when talking about scope. Another factor is the location in the program where the declaration occurs. Based on this, variables fall into one of two categories — global variables and local variables.
Global variables are those which are declared in the top-level scope. This means that their declaration does not occur inside of a block or function body. Since all variables can be access from an inner (or more narrow) scope relative to where they are declared, global variables are accessible everywhere in a program.
Local variables are those which are declared inside a block or function body. They are said to have local scope because they can only be accessed inside that artifact, and any that are more narrowly scoped.
Variable Shadowing
When a variable is declared in an inner scope with the same name as another variable in an outer scope, variable shadowing occurs. This causes the variable in the outer scope to become inaccessible. All references which use the variable name point to the variable that was declared in the inner scope. Getting confused yet? An example will help:
After declaring a variable dog
on line 1, we have another variable declaration with the same variable name on line 4. Since this happens in an inner scope, a new dog
variable is declared and initialized to 'Ruby'
. This is a different variable than the dog
that was declared on line 1. We see evidence for this in the remaining code. Although dog
has a value of 'Ruby'
when line 5 executes, when dog
is referenced outside of the block on line 7, the variable from line 1 is accessed and reports a value of 'Charlie'
.
Here is another scenario where variable shadowing can occur:
Under normal circumstances, the variable dog
which is declared on line 1 would be accessible inside of the petTheDog
function. However, in this case the function parameter has the same name as this variable. Since function parameters create local variables (local to the function), the parameter dog
shadows the variable dog
from the outer scope. That is why when dog
is referenced inside the function on line 4, it returns the value of the argument passed to the function instead of the value of the dog
global variable.
We have made it through a whirlwind introduction to one of the most fundamental topics in JavaScript. Understanding variable scope is essential to crafting robust programs in any language. JavaScript is no exception.
One of the beautiful facets of JavaScript is that it is a forgiving language in many ways. A functional program can be hack-and-slashed together with minimal knowledge of how the language works. But as with any language, building something capable, maintainable, and long-lasting is much easier if you understand the language well. It can also be remarkably fun.
This article is a precursor to my articles on hoisting and closures in JavaScript. If those topics interest you, feel free to check them out!