DEV Community

Cover image for All I Know About: Scope in JavaScript
Keith Charles
Keith Charles

Posted on

All I Know About: Scope in JavaScript

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode
// fileB.js
let count = 2;
Enter fullscreen mode Exit fullscreen mode
// fileC.js
console.log(count); //-> ???
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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"
}
Enter fullscreen mode Exit fullscreen mode

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"
  }
}
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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.
Enter fullscreen mode Exit fullscreen mode

Whereas let can be changed how ever many times you want.

let lastName = "Charles";
lastName = "Richards";
lastName = "Urban";

// no errors with this!
Enter fullscreen mode Exit fullscreen mode

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"
  }
}
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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";
Enter fullscreen mode Exit fullscreen mode
// fileB.js
console.log(myName); //-> ReferenceError: myName is not defined
Enter fullscreen mode Exit fullscreen mode

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}
Enter fullscreen mode Exit fullscreen mode
// fileB.js
import {myName} from 'fileA.js';
console.log(myName); //-> "Keith"
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

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 returnfuncBat the end offuncA. Outside of the functions we invokefuncAand set it inside of ourlexicalScopevariable. This will give usfuncBin return. Finally, we console logsomeVarwhich gives us our global variable value. And we console log the invocation offuncB` 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)