JavaScript: Scope
Posted by in Programming onJust about everyone's lost their cell phone at one point or another. You reach into your pocket to find your phone, only to realize that it's been misplaced. You quickly check your other pockets, and it's not there either. The next step, you check your immediate surroundings — but it's not there, either. You check the room you are in. Then you check the previous room you were in. You retrace your steps as far back as you need to until you eventually recover your trusty miniature-super-computer.
It turns out that this is basically the same process that JavaScript uses whenever you reference a variable. The Computer Science term for this process is called scope. Before you can become proficient with any language, you need to understand how scoping works in that language and JavaScript is no exception. So let's get to it!
What is Scope?
A technical — albeit highly simplified — definition of scope could be "The set of variables and functions that may be accessed from any given line of code at run-time". A less technical definition (which won't win you any awards for completeness) would be that "scope defines which variables a line of code can read or write while the program is running". But who really cares about definitions? Being able to recite a definition is less important than being able to practically implement the underlying idea being defined.
Scope gives you, the programmer, the ability to limit access to a variable or function and prevent tampering. If you take advantage of JavaScript's scope and leverage it properly, you will be able to contribute to large projects without having to worry about overwriting functions or variables elsewhere in the code:
var a = 'a';
function redFish() {
var a = 0;
}
function blueFish() {
var a = 1;
}
One thing to note is that Scope is not Context. In JavaScript, the this
keyword lives by its own rules; but that's a topic for another post.
Types of Scope
From a Computer Science perspective, there are several different ways to categorize scope. The three main terms to remember here are Scope Chain, Lexical Scope, Global Scope and Local Scope.
Scope Chain
The Scope Chain is the hierarchy of scopes that will be searched in order to find a function or variable. In the cellphone analogy, this chain iterates through the "normal pocket", to the "other pockets", to the "immediate area", and so on until the phone is found or you run out of places to look. In computing, this pattern is defined by the language itself — which brings us to Lexical Scope.
Lexical Scope
Another Computer Science term, Lexical Scope describes a method of resolving the Scope Chain based on how a file is constructed. This makes it easier to determine the scope that applies to a line of code simply by looking at how the file is constructed. Consider the following example:
function createCounter() {
// updated by increment() and decrement()
// but hidden from everything outside the function
var counter = 0;
function increment() {
return ++counter;
}
function decrement() {
return --counter;
}
return {
decrement: decrement,
increment: increment,
};
}
// available to everything
var counter = createCounter();
When counter.increment()
is called, the increment
function will start looking for a counter
variable. There is no counter inside of increment()
, and so it checks the parent scope — createCounter()
. In the createCounter
function, there is a variable named counter
, and so that variable is used. The scope chain never needs to look further, and so the global counter
variable is never reached — the technical term for this is Name Masking. If, however, no matching variable name can be found in the scope chain, then JavaScript will consider that variable to be undefined
.
Since the variable defined with var counter = 0
is accessible to the object returned by createCounter()
, it will be immune to garbage collection until that object is garbage-collected. Also, since createCounter()
allocates memory for a new variable every time the function is run, it means that multiple counters can be created without interacting with each other.
Global Scope
Global Scope is somewhat unique. Since global variables are in the top-level scope, and the top-level scope is accessible by all other scopes, every variable in the global scope is accessible from every other line of code in a project. The impact is that global variables are never garbage-collected. They stay alive in memory until the program exits.
In some cases, this is the desired behavior. For example, you wouldn't want the window.document
object being garbage-collected in a browser or the global.require
function to be garbage-collected in Node.js.
Usually, this isn't what you really wanted to do, though. Since global variables are accessible everywhere, it means that the variables are writable everywhere. In practice, this means that you cannot guarantee that a variable will be valid when you try to access it. You can avoid this through the judicious use of closures (an article for another day), and avoid assigning to variables that haven't been declared.
function createsGlobal() {
// This is normal in Python, but creates a global variable in JavaScript
myGlobal = true;
}
In the above example, if no variable myGlobal
can be found, it will be created in the global scope. Again, this is almost always a mistake. Even if you're doing it deliberately, it looks like a mistake, and you should make your intentions clear: global.myGlobal = true
.
Local Scope
So now we get to talk about Local Scope. In a nutshell, everything that isn't global scope is considered local scope; meaning that a given line of code is obscured by at least some part of a project.
This saves us from some of the nastier features of global scope. We can control what parts of an environment may access our variables, we can avoid overwriting important lines of code elsewhere, and we can make sure that the garbage collector will clean up after us.
In case you were wondering, though: yes, Local Scope can be further divided. The three things you'll really need to know before you can understand how local scope works are Hoisting, Block Scope, and Function Scope.
Hoisting
If you are coming to JavaScript from another language, and you plan to work in a browser, then Hoisting is going to be a bit of a shock. When JavaScript runs a function, it will parse that function to find all instances of the function
and var
keywords, and then assign memory addresses for those variables. The result is that these variables are always treated as if they were declared at the top of their containing function:
obvious(); // false
notObvious(); // true
function obvious() {
var x = true;
x = false;
return x;
}
function notObvious() {
x = false;
var x = true;
return x;
}
x; // undefined
This is another subject that I plan to cover in another article, but it is important to understand that neither function assigns a value to the global scope.
Function Scope
Function Scope refers to the set of variables that are created within a function. In the Hoisting example, above, the variable x
is obscured from the global scope and returns undefined
if a line of code outside of obvious
or notObvious
attempts to access it. This is the oldest and most common method for creating local scopes in JavaScript.
// We can run "firstLayer" from here
function firstLayer() {
// We can run "firstLayer" and "secondLayer" from here
function secondLayer() {
// We can run "firstLayer", "secondLayer", and "thirdLayer" from here
function thirdLayer() {
return true;
}
}
}
Block Scope
Block Scope is a type of Local Scope where a variable is only accessible within a code block and is a relatively new concept to JavaScript. JavaScript defines a block of code as anything that exists within {curly brackets}
. However, this also holds true for single-line instructions for loops and branching logic. A variable defined with let
or const
within a block of code will only be accessible within that block of code. This also means that the value of a let
or const
variable within a loop will retain whatever value existed within that scope.
const container = document.getElementById('button-container');
for (let i = 1; i <= 4; ++i) {
const button = document.createElement('button');
button.addEventListener('click', () => alert(`You pressed Button #${i}`));
button.innerHTML = `Button #${i}`;
container.appendChild(button);
}
container; // valid object
i; // undefined
button; // undefined
Conclusion
If you've been playing around with JavaScript functions and/or the let
/const
keywords already, you may have already intuited some of these scoping behaviors for yourself. The main benefit that scoping provides, is it gives you (the programmer) a framework in which you can control what lines of code may access your variables. When used appropriately, scoping helps you contribute code to large projects without having to worry about collisions with other developers' work. Even if this isn't an immediate concern or goal of yours, it also means that you can write small libraries for a website or NPM without having to worry about collisions with third-party packages.
Happy scoping!