Scope
Variables in programming are used either to track state across a program's lifecycle (like a logged in or logged out state) or to store constant values that need to be used later (like an API key). Being able to access a variable, whether to give it value, redefine its value, or use its value, is fundamental to the lifecycle of an effective program. Much of what I'm writing about today was covered in my March blog post, var, let, and const, but today's post shifts the focus a little bit to the topic of scope.
Scope refers to a set of rules for how variables and their values get accessed throughout the lifecycle of a program. For example:
var ovenOn = false;var ovenTemperature = 70;
function heatOven(targetTemperature) { if (ovenOn === false) { ovenOn = true; console.log("Oven has been turned on."); }
while (ovenTemperature < targetTemperature) { ovenTemperature += 1; }
console.log(`Oven has been heated to ${ovenTemperature}.`);}
function turnOvenOff() { if (ovenOn === true) { ovenOn = false; } console.log("Oven has been turned off.");}
heatOven(350);// expected result:// Oven has been turned on.// Oven has been heated to 350.
turnOvenOff();// expected result: Oven has been turned off.
In this little program, we're working with two variables: ovenOn
and ovenTemperature
. These are declared using the var
keyword outside of any other functions, so they are said to belong to the global scope, making them (and their values) accessible throughout the program. When we later make references to ovenOn
or ovenTemperature
inside of function definitions, we can use and modify those variables.
Let's look at another example:
function logUserIn() { var loggedIn = true;}
logUserIn();
if (loggedIn) { console.log("User is logged in."); // this code never runs}// expected result: ReferenceError: loggedIn is not defined
Here we have a function logUserIn()
, and inside of that function block we declare a variable loggedIn
and assign it the value of true
. We then call the logUserIn()
function and try to check the loggedIn
variable value to log the message "User is logged in."
if loggedIn
is true
. However, we get a ReferenceError
that loggedIn
is not defined. This happens because we declared the loggedIn
variable inside of the logUserIn()
function, but tried to access it outside of that function. The loggedIn
variable is said to be at the logUserIn()
function scope, and our access attempt would be outside of that function, in the global scope; so we are trying to access that variable outside of its scope.
Nested scopes are available in JavaScript:
function firstFunction(val1) { var val2 = val1 + 100; function secondFunction(val3) { console.log(val1, val2, val3); // expected result: 420, 520, 720 } secondFunction(val2 + 200);}
firstFunction(420);
In this example there are three levels of scope:
- The outermost scope, the global scope
- The scope of the first function,
firstFunction()
- The scope of the next inner function inside of
firstfunction()
,secondFunction()
In the global scope the only identifier available is the firstfunction()
function.
Within firstFunction()
's scope, we define the val1
identifier, which is implicitly defined as a parameter assignment, and given the value 420
when the function is called with 420
as its argument, the val2
identifier, explicitly declared using the var
keyword and given the value of 100
plus the value of val1
(420
), so 520
, and the secondFunction()
identifier.
Within secondFunction()
's scope, we define the val3
identifier, implicitly defined as a parameter assignment, and given the value of 720
when the function is called and val2 + 200
is given as its argument. However, because secondFunction()
is nested within firstFunction()
and firstfunction()
is nested within the global scope, we can access all identifiers in that nesting chain within secondFunction()
. This is what gives us all three values 420
, 520
, 720
when we run console.log(val1, val2, val3);
.
We can also redefine variables from an outer scope in an inner scope:
function firstFunction(val1) { var val2 = val1 + 100; function secondFunction(val3) { val1 = 429; val2 = 666; console.log(val1, val2, val3); // expected result: 429, 666, 720 } secondFunction(val2 + 200);}
firstFunction(420);
This time around we redefine the values of val1
and val2
to 429
and 666
inside of secondFunction()
, but those values are still 420
and 520
in the firstFunction()
scope, so we still get 720
for val3
. But now when we console.log(val1, val2);
inside of secondfunction()
the values within that scope are used (instead of the values of the outer scope). This behavior is called "shadowing". It's a powerful way that we can fine-tune the values of our variables across the life cycle of our program at different scopes where they may be needed.
Units of Scope
Now that we have a basic understanding of scope, let's dive into the four units of scope available in JavaScript: global scope, function scope, block scope, and module scope. We have seen examples of the first two already, but I'll focus on each unit individually and get into behavior specific to them.
Global Scope
If a variable has been declared outside of any other function or (for let
and const
variables) code block, it is said to be in the global scope. This is the top level of a program, and within a browser is called the window
. Variables declared at the global scope using the var
keyword can either be called directly or as properties on the global object:
var globalThing = "Howdy 123";
console.log(globalThing);// expected result: "Howdy 123"console.log(window.globalThing);// expected result: "Howdy 123"
Identifiers at the global scope are available at all other scopes, since any other function or block scope is nested inside of the global scope:
var globalThing = "Howdy 123";
function firstFunction() { console.log(globalThing); // expected result: "Howdy 123"
function secondFunction() { console.log(globalThing); // expected result: "Howdy 123" } secondFunction();}
firstFunction();
A var
variable at the global scope can be redeclared in a nested function. If it's just being redeclared, then the original value of the global identifier can still be accessed as a property on the global object:
var globalThing = "Howdy 123";
function firstFunction() { var globalThing = "Hey there 321"; console.log(globalThing); // expected result: "Hey there 321"
function secondFunction() { console.log(globalThing); // expected result: "Hey there 321"
console.log(window.globalThing); // expected result: "Howdy 123" } secondFunction();}
firstFunction();
When you're not working in strict mode, you can create variables by omitting the var
, let
, or const
keyword, and just assigning that variable name a value. It will be created like any other var
variable:
globalThing = "Howdy 123";
console.log(globalThing);// expected result: "Howdy 123"console.log(window.globalThing);// expected result: "Howdy 123"
However, this is generally considered poor programming practice; if you are creating a variable, you should communicate intent to do so using one of the declaration keywords (and thereby determine how you want that variable to behave, i.e. as a var
, let
, or const
variable). Because of this, strict mode will throw a ReferenceError
if you attempt to do so:
"use strict";globalThing = "Howdy 123";// expected result: ReferenceError: globalThing is not defined
This helps to prevent accidental scope pollution if you have a typo:
"use strict";var globalThing = "Howdy 123";globalThingg = "Hey there 321";// expected result: ReferenceError: globalThingg is not defined
The question of what should be in global scope vs. what shouldn't be goes well beyond mistyped variable names. A larger, related question here is: what information needs to be accessed across the entire program? There are two main problems with having all variables and their values available across the global scope:
- privilege: some information probably needs to be kept "secret" as it were, or at least should be contained only to where it's relevant so that it doesn't get overwritten or accessed when it shouldn't be;
- memory: if you have a bunch of data objects scattered throughout your global scope, it can start to take up a lot of memory. Ideally you would use the data only when you need it, and when you no longer need it, the JavaScript engine would safely carry that data off to garbage collection.
Fortunately for us, two other scope units exist that help to address these two concerns: function scope and block scope.
Function Scope
We've already seen some examples of function scoped variables, but to review, variables (and other functions) declared within a function are said to be function scoped, and are therefore available to access within that function:
function firstFunction() { var val = 420; console.log(val); // expected result: 420}
firstFunction();
Variables declared within a function are available to functions that are also declared in that function, illustrating the concept of nested scope:
function firstFunction() { var val = 420; function secondFunction() { console.log(val); // expected result: 420 } secondFunction();}
firstFunction();
However, variables declared within a function are not available outside of that function, for example in the global scope:
function firstFunction() { var val = 420;}
firstFunction();console.log(val);// expected result: ReferenceError: val is not defined
In this way, function scoped identifiers are one solution to the global scope pollution problem. We are able to keep variables and their data contained to the functionality where it's needed, and it doesn't bleed into the global scope.
However, what if we do want some data available at the global scope in order to get a little bit of work done, and then we don't want that data to stick around? Like we need some variables and functionality to run in order to initialize our program (like load a Google Analytics tracking script), but then once that functionality is done running, those variables and functions no longer need to take up space in the global scope? That is where Immediately Invoked Function Expressions (IIFEs) come in.
For a more thorough exploration into IIFEs, check out my blog post from May, What is an IIFE?. As it pertains to the topic of scope, IIFEs are a handy way of avoiding global namespace pollution.
Say we want to greet a user by name:
var userName = prompt("What is your name?"); // input "Joey"
function sayHello(name) { console.log(`Hi there ${name}!`);}
sayHello(userName);// expected result: "Hi there Joey!"console.log(window.userName);// expected result: "Joey"console.log(sayHello);// expected result: function sayHello(name) { ...etc.. }
In this example we've created a var
variable, userName
, and assigned it the value that a user inputs to the prompt, 'What is your name?'
.
After that we define a function named sayHello()
, which accepts a name
parameter, and greets the user by name.
Below that, we run the function, passing it the userName
variable as its argument.
Finally, we console.log(window.userName)
and sayHello
and see that we have polluted our window
scope with the userName
variable and the sayHello()
function.
If we're only going to run this code once and never again, it doesn't make a lot of sense to have the window
carrying around a userName
variable or the sayHello()
function in its global scope for the entire time our application runs. Instead, we can use an IIFE and not pollute the global scope:
(function sayHello() { var userName = prompt("What is your name?"); console.log(`Hi there ${userName}!`);})();console.log(window.userName);// expected result: undefinedconsole.log(sayHello);// expected result: ReferenceError: sayHello is not defined
Because var
variables are scoped to the function that they're declared in, the userName
variable only exists within the sayHello()
function, which only exists within the IIFE. This is key to understanding the benefits of IIFEs: that before the introduction of let
and const
variables or the arrival of ES Modules and CommonJS, the IIFE enabled a developer to execute code that had a safe scope without polluting the global scope or creating an entire other function (along with a function call).
Block Scope
By now we're well familiar with global scope pollution traps set by var
variables. But wouldn't it be nice to be able to create portions of code that aren't functions where variables exist? For example:
var loggedIn = true;
if (loggedIn) { var username = "Joey";}
It seems like a perfectly reasonable expectation that the username
variable should only be declared and brought into scope if loggedIn
is true
. However, that's just not the case with var
variables. It turns out that we've polluted our global scope:
var loggedIn = true;
if (loggedIn) { var username = "Joey";}
console.log(username);// expected result: "Joey"
A similar issue gets observed with when we use a var
variable in the initialization statement in the head of a for
Loop:
for (var i = 0; i <= 10; i++) { // doing some work}
// we're able to access i outside of the loop:console.log(i);// expected result: 11
To address these issues, we can utilize block scoped variables. Variables that are only available within a set of curly brackets {}
are said to be block scoped. The code within a set of curly brackets {}
is referred to as a code block. Using the let
and const
declaration keywords instead of var
is one way to achieve a block scoped variable.
Let's go back to our loggedIn
example:
var loggedIn = true;
if (loggedIn) { const username = "Joey"; console.log(username); // expected result: "Joey"}
console.log(username);// expected result: ReferenceError: username is not defined
Now username
is only accessible in the scope of the if
conditional block! Neat. Now let's look at the for
Loop example again:
for (let i = 0; i <= 10; i++) { console.log(i); // expected result: 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
// we're able to access i outside of the loop:console.log(i);// expected result: ReferenceError: i is not defined
This time around i
is only available within the for
Loop block and re-binds itself to the head of the loop as long as the loop runs.
let
and const
are neat ways to help avoid global scope pollution without having to lean on IIFEs.
We can also create code blocks ad hoc:
{ const hugeDataObject = [ // some huge data block, like an array of objects ];}
In this silly little pseudocode example we've used a const
variable to scope a fictitious data array to a block of code. Assuming we are doing some work within the {}
block, this arrangement tells the JavaScript engine that once that work's done, it's safe to do garbage collection on hugeDataObject
, freeing up memory and allowing the engine to optimize itself.
It's worth noting that let
and const
variables, when declared outside of any other code block, are considered to be globally scoped. However, global let
and const
variables are not available as properties on the global object/window
in the way that var
variables are:
var var1 = "Howdy";let var2 = "Hello";const var3 = "Hi";
console.log(window.var1);// expected result: "Howdy"
console.log(window.var2);// expected result: undefined
console.log(window.var3);// expected result: undefined
The with
keyword is another, older way to achieve block scoping, although its usage is considered an anti-pattern.
with
is a shorthand way to update multiple properties on an object:
var person = { age: 33, favoriteNumber: 66,};
with (person) { age = 34; favoriteNumber = 666;}
console.log(person.age);// expected result: 34
console.log(person.favoriteNumber);// expected result: 666
What's happening here is that the with
keyword creates a new scope out of the object it's given. A quirk of this behavior is notable if you attempt to update a non-existent property on the object:
var person = { age: 33, favoriteNumber: 66,};
with (person) { secondFavoriteNumber = 420;}
console.log(person.secondFavoriteNumber);// expected result: undefined
console.log(secondFavoriteNumber);// expected result: 420
console.log(window.secondFavoriteNumber);// expected result: 420
Because with
creates its own lexical scope, it treats a non-existent property it's trying to access on its given object as any other identifier; outside of strict mode it creates that property as an identifier, not on the object it's given but at the global scope. This is why we're not able to access person.secondFavoriteNumber
, but we are able to access secondFavoriteNumber
and window.secondFavoriteNumber
.
This behavior is an anti-pattern, and disallowed in strict mode (just like creating variables without using a declaration keyword).
Another, lesser known way of achieving block scope is within the catch
portion of a try...catch
statement:
try { someFunctionThatDoesntExist();} catch (error) { console.log(error); // error works here}
console.log(error);// expected result: ReferenceError: "error" is not found
Module Scope
ES6 introduces first class support for modules, which is a way to organize functionality in your code. You can bundle common functionality into a module file, export the data or functionality you want from that module, and then import that exported data or functionality elsewhere in your program.
Say you have a function greeting()
in a greeting.js
file that is located within a modules
directory. If you want to use that greeting()
functionality elsewhere, you can now export it:
// modules/greeting.js:export function greeting(name) { console.log(`Howdy ${name}`);}
By using the export
keyword, you are making that function available to be imported in other files in your program. For instance, in a top-level index.js
file:
// index.js:import { greeting } from "./modules/greeting.js";
greeting("Joey");// expected result: "Howdy Joey"
For folks who have worked with JavaScript prior to ES6, this is a pretty radical change in how we can write and share functionality. This post will not get into all of the details of modules (that could very well be its own post or series of posts), but there are implications for our discussion of scope. When you create a module file it creates its own scope.
// modules/coolNumbers.js:var count = 420;var coolNumber = 66;
function logCount() { console.log(count);}
export { coolNumber, logCount };
We have created a new module called coolNumbers
. In it we have two variables (count
, with the value of the number 420
and coolNumber
, with the value of 66
) and one function, logCount()
, which will simply log the value of count
. We are exporting coolNumber
and logCount
, but not count
.
Back in index.js
, let's import coolNumber
and logCount
:
// index.js:import { coolNumber, logCount } from "./modules/coolNumbers.js";
console.log(coolNumber);// expected result: 66
console.log(count);// expected result: ReferenceError: count is not defined
We can easily log out the value of coolNumber
, but since we didn't export count
and it's not available for import, we predictably get a ReferenceError
when we try to use count
. However, if we call the logCount()
function that we import:
// index.js:import { coolNumber, logCount } from "./modules/coolNumbers.js";
logCount();// expected result: 420
...then we get 420
. What this demonstrates is that variables and functionality that are defined within a module are scoped to that module. In this case, count
is not exported, but is available within the module that it's defined in, so logCount
can still access its value. Even if something is not made publicly available through export, it is available privately within that module and can still be worked with. This is an example of module scope.
Other peculiarities with modules and scope:
import
andexport
statements must be done at the top-level scope of their usage. In order words, you cannot do conditional importing or exporting within anif
statement inside of a module- a module file does have access to a
window
or global scope, but accessing the global scope from inside of a module is frowned upon.
Summary
- Scope refers to a set of rules for how variables and their values get accessed throughout the lifecycle of a program. There are four units of scope: global scope, function scope, block scope, and module scope
- If a variable has been declared outside of any other function or code block, it is said to be in the global scope; these are accessible throughout the program
- All variables (declared using
var
,let
, orconst
) can be globally scoped, but onlyvar
variables are accessible as properties on the global object
- All variables (declared using
- Variables declared within a function are said to be function scoped; these are accessible within the function they're declared and any functions nested within that function
var
variables declared within a function are function scoped- Scoping variables and functionality within functions, including Immediately Invoked Function Expressions (IIFEs), is a strategy for avoiding scope pollution
- Variables that are only available within a set of curly brackets
{}
are said to be block scopedlet
andconst
variables declared within a block are block scoped- The
with
keyword also does block scoping, but it's an anti-pattern to use this technique (and disallowed in strict mode) - The
catch
clause in atry...catch
statement is also block scoped
- Variables declared within a module are said to be module scoped. These are accessible within the module that they're defined, or publicly if they are exported from that module