Based off some of the bootcamp students I tutor, "scope" is either not something taught in their curriculum, or at most, briefly touched upon. However, understanding scope plays a huge factor in being able to debug and fix your own code. I'm here to shine a light on scope in JavaScript and why it's such an important concept to understand. If you're someone who understands each line of the code below, but you're unable to tell why it returns an error, you've come to the right place!
if(true) {
let someVar = "Foo"
}
console.log(someVar) //-> ReferenceError: someVar is not defined
Defining Scope
Before discussing the code above, let's actually define scope. Here's what I ripped right out of MDN's documentation:
Scope - The current context of execution. The context in which values and expressions are "visible" or can be referenced.
Make sense? No? Don't worry, MDN's documentation takes some getting used to and may not make sense when you're just starting out. Let me break down some of the words.
First, "referencing" a variable. Some of you may understand the difference, but let me quickly explain declaring, defining, and referencing.
// this is declaring, but not defining
let myVar;
// this is declaring and defining on a single line
let otherVar = 10;
// this is referencing a variable that has already been declared
console.log(otherVar); //-> 10
// this is referencing a previously declared variable
// and defining its value
myVar = 50;
// this is referencing a previously declared variable
// and re-defining its value
otherVar += 20; //-> otherVar now equals 30
Referencing a variable is calling a variable that has already been declared before. If you try and reference a variable that hasn't been declared yet, you get an error. Likewise, if you reference a variable that has been declared but hasn't been defined, you'll get an undefined value and no error. Like so:
let myVar;
// try to reference a variable that was never declared
console.log(otherVar); //-> ReferenceError: otherVar is not defined;
//try to reference a variable that WAS declared but never defined
console.log(myVar); //-> undefined
In the case of context, just think of it as the surrounding rules of how code is read or how a variable is used. Without context, any variable could be read from any part of a js file, or worse, if we have multiple files, a variable could be declared in one file but re-defined or referenced in a different file even if that wasn't our intention. It would be anarchy! Example:
// fileA.js
let count = 10;
// fileB.js
let count = 2;
// fileC.js
console.log(count); //-> ???
Without any context telling us the rules for each count
there would be no way to tell fileC
which count
to log since we have two count
variables from two different files. And that's what scope is. It's just giving our code some context as to how and where our variables can be referenced. Once we get into the types of scope, this will all start setting in.
Types of Scope
JavaScript has a handful of different kinds of scope. One way we can tell our code what kind of scope we want to use is by adding a var
, let
, or const
before the variable name when declaring our variable. This keyword is what tells JavaScript how we want to scope the variable.
Block Scope: let
and const
We'll talk about let
and const
first since it's considered the new standard after their premiere in ES6 and they're probably what you're using right now anyway. I'll explain what ES6 is in a later post, but for now just know it is a feature release made by the top brass who are hard at work, standardizing JavaScript along with other languages.
let
and const
variables use what we call block scope. Anytime you've ever seen curly braces in your code, that represents a block of code. Block scope means that your variable is only readable and writeable within the block it was declared in. This is a perfect time to bring back our problem at the very beginning! Let's look at that again:
if(true) {
let someVar = "Foo"
}
console.log(someVar) //-> ReferenceError: someVar is not defined
Notice how someVar
is declared inside of the curly braces of the if statement, but we try to call the variable outside of those curly braces. Block scope tells JavaScript that we only want our variable to exist inside of the block it was declared in. Anything outside of the block will have no reference to the variable in the block, hence the ReferenceError
we're getting. If we were to move the console log inside of the block, we would be able to log someVar
since it would be within the scope:
if(true) {
let someVar = "Foo"
console.log(someVar) //-> "Foo"
}
Likewise, if we had child blocks in our block, that is, if we had other blocks inside our block, those children will have access to variables declared in their parent.
// parent block of code
if(true) {
let color = "orange";
// child block of code inside parent
if(true) {
console.log(color); //-> "orange"
}
}
No matter how many children or grandchildren the parent block has, the children will always have access to variables declared inside any of their parents, grandparents, etc. However, parent blocks of code cannot reference variables that were declared in one of their children.
if(true) {
if(true) {
if(true) {
// create variable in a child block
let color = "green";
}
}
// try to reference the variable
// at a parent block
console.log(color); //-> ReferenceError: color is not defined
}
So what if we need to define a variable in a child block, but then reference that variable in a parent block? Let's say you have a function (parent block) and in the function you want to create a variable if some condition is true, but you still have to return the variable at the end of the function. All you have to do is declare the variable in the parent block before the child block:
//parent block
function someFunc() {
// declare variable in parent block
let myVar;
if(true) {
// define variable in child block
myVar = "It was true!";
}
// reference variable back in parent block
return myVar;
}
As you can see, even though we defined myVar
in a child block, we can reference it in the parent block because it was declared in the parent block.
You might be wondering what the difference is between const
and let
is since they both have the same exact scope. While it's true that they both share the same scope, const
variables cannot be mutated from its original definition. For instance:
const firstName = "Keith";
firstName = "George"; //-> TypeError: Assignment to constant variable.
Whereas let can be changed how ever many times you want.
let lastName = "Charles";
lastName = "Richards";
lastName = "Urban";
// no errors with this!
This helps to store data and prevent it from ever being changed, such as storing a url like "http://facebook.com"
. It's pretty safe to assume facebook's url will never change, so to give your code some added security we can store that url in a const variable, and we'll sleep soundly knowing a new line of code won't ever inadvertently change the value of that variable.
Global Scope: var
, let
, and const
When a variable is declared outside of any function or block of code, regardless of if you're using var
let
or const
, it is considered Globally Scoped. What this means is that any inner scope has access to reference a globally scoped variable. Example:
// variable declared outside of any function or block
let iceCream = "chocolate";
console.log(iceCream); //-> "chocolate"
if(true) {
console.log(iceCream); //-> "chocolate"
}
function giveMeIceCream() {
console.log(iceCream); //-> "chocolate"
if(true) {
console.log(iceCream); //-> "chocolate"
}
}
No matter where you are in your code, you will always have access to globally scoped variables. Again, using const
would have the same effect as let
, as would var
in this case. However var
goes a little further, adding your variable as a property of the global window
object. Here's an example:
var myName = "Keith";
console.log(window.myName); //-> "Keith"
This is the truest form of "Global" as the window
object is always accessible no matter where you are in your file, and no matter what file you're in inside your app/website.
Functional/Local Scope: var
var
is the only keyword that creates a Functional Scope also known as Local Scope. That just means that a variable declared inside of a function can be referenced anywhere within that function, regardless of any blocks that may be in the code. Example:
function myFunc() {
if(true) {
// declare variable with var (function scope)
var someVar = "Bar";
}
// can call any var variable within the same function
// regardless of block difference
console.log(someVar); //-> "Bar"
}
myFunc();
// someVar only exists within the function
// it was declared inside of
console.log(someVar); //-> ReferenceError: someVar is not defined
In the example, we can see how functional scope differs from block scope. With block scope (if we declared the variable with a let
instead of a var
, the first console log would result in an error because the log is outside of the if statement where the variable is declared, but with functional scope we can access the variable anywhere within myFunc
. As for the other console log outside of myFunc
, we get an error because we're outside of the function, therefore outside of the scope of someVar
.
Other Scopes
Once you've grocked all that we discussed above, we can get into the slightly more complicated versions and aspects of scope in JavaScript.
Module Scope
If you've used JavaScript libraries like React or if you've used ES6 modules where you export
parts of one js file and then import
them into another file, then you've run into Modular Scope. Modular scope prevents code from accessing variables or functions from other files unless you explicitly export
that variable from the file and then import
it to the file you're trying to use it in. Here's an example without modular scope:
// fileA.js
const myName = "Keith";
// fileB.js
console.log(myName); //-> ReferenceError: myName is not defined
Here, fileB
has no idea what myName
is, therefore it cannot log it from within the bounds of its file. However if we were to export
myName
from fileA
then import it to fileB
:
// fileA.js
const myName = "Keith";
export {myName}
// fileB.js
import {myName} from 'fileA.js';
console.log(myName); //-> "Keith"
Now that fileB
knows where to grab myName
from, we can easily access the variable and call it whenever we want from fileB
.
Lexical/Static Scope
Lexical scope also known as static scope deals with functions within functions, or nested functions. When you nest functions together the variables inside those functions use the scope that was in place when the functions were first defined. For example:
let someVar = "I'm global scoped!"
function funcA() {
let someVar = "I'm block scoped"
function funcB() {
console.log(someVar);
}
return inner;
}
const lexicalScope = outer();
console.log(someVar); //-> "I'm global scoped!"
console.log(lexicalScope()); //-> "I'm block scoped"
So what the heck is going on here? Let's break it down. we first define someVar
globally. Then we create funcA
and in it, redefine someVar
as a block scoped variable. Next we create funcB
that just logs someVar
which we're grabbing from funcA due to block scoping (
someVaris declared in a parent block so we can access it in a child block). Then we return
funcBat the end of
funcA. Outside of the functions we invoke
funcAand set it inside of our
lexicalScopevariable. This will give us
funcBin return. Finally, we console log
someVarwhich gives us our global variable value. And we console log the invocation of
funcB` which gives us our block scoped variable.
We're calling funcB
outside of funcA
, so how are we still accessing the someVar
inside of funcA
? Well, I'll reiterate: When you nest functions together the variables inside those functions use the scope that was in place when the functions were first defined. When funcB
was first defined, the scope of someVar
was block scoped because of the variable we declared in funcA
which was the parent block of funcB
. Therefore, whenever we call that nested inner function, funcB
, we grab the variable it referenced when it was first defined, not the globally scoped variable.
Wrapping it up
As you can see, there are a number of different scopes to keep in mind when coding in JavaScript. Don't worry if you need to come back to this as a reference from time to time! It will take a minute before you get a full grasp of every kind of scope JavaScript touts. Just keep an eye on where you're declaring your variables, and remember what scope the keyword you're using encompasses. (But you should really be using let
and const
at this point!) ✌
Top comments (0)