const - JS | lectureHow JavaScript Works Behind the Scenes

Scope and The Scope Chain

How JavaScript Works Behind the Scenes

We learned in the last lecture that each execution context has a variable environment, a scope chain, and a this keyword. In this lecture, let's learn what scope and a scope chain are, why they are so important, and how they work.

Let's start by understanding what scoping means and learn about some related concepts.

Scoping controls how our program's variables are organized and accessed by the JavaScript engine. So basically, scoping asks the question:

Where do variables live? Or where can we access a certain variable and where not?

In JavaScript, we have something called lexical scoping. Lexical scoping means that the way variables are organized and accessed is entirely controlled by the placement of functions and blocks in the program's code. For example, a function written inside another function has access to the parent function's variables. In other words, variable scoping is influenced by where we write our functions and code blocks. Now, let's talk about scope itself.

Scope is the space or environment in which a certain variable is declared, simple as that. In the case of functions, that's essentially the variable environment stored in the function's execution context.

If now you're asking yourself, what's the difference between scope and variable environment? Then the answer is that for the case of functions, it's basically the same. There exist three types of scopes in JavaScript: global scope, function scope and block scope. Before talking about them, let's also define what the scope of a variable is.

The scope of a variable is the entire region of our code, where a certain variable can be accessed.

Some people use the word scope for all of this but, defining all these concepts clearly shows that there are actually subtle differences. For example, if you take a close look at it, scope is not the same as scope of a variable, and I guess you've seen the subtle differences. I know it might still sound all the same for now, but after looking at a couple of examples and writing some real code, you will understand everything I just showed you here.

Let's now talk about the three different types of scope in JavaScript. That is the global scope, function scope, and block scope. And remember, scope is the place in our code where variables are declared. When I say variables, the same thing is true for functions because, in the end, functions are just values stored in variables.

First, the global scope is once more for top-level code. This is for variables that are declared outside of any function or block. These variables will be accessible everywhere in our program, in all functions and blocks.

Global Scope

Next, each and every function creates a scope, and the variables declared inside that function scope are only accessible inside that function. This is also called a local scope as opposed to global scope. So, local variables live in the function, so to say and outside of the function, the variables are then not accessible at all.

Function Scope

This is technically the same as the function's variable environment, but we still need to give it the name of scope in this context because blocks also create scopes.

In the above example, the secretNumber variable receives a random number between 1 and 20 inside the generateSecretNumber function. We can then use it to do whatever calculation we would like to, even if, in this case, we immediately return the variable. But outside of the function, we get a reference error as we try to log it to the console.

We get this error because JavaScript is trying to find the secretNumber variable in the global scope, so outside of the function, but it cannot find it, thus giving us the reference error.

If you remember our pig game project, that is also the reason why we had to declare a couple of variables outside of the initGame function. Remember that? We had some variables declared in the initGame function, which gave us an error because other functions were trying to access these variables.

They were in the function scope, which means they were locally scoped, and so we couldn't access them outside of that function where they were declared.

It does not matter what kind of function we're using. Function declarations, function expressions, and arrow functions all create their own scope. Traditionally, only functions used to create scopes in JavaScript. But starting in ES6, blocks also create scopes now.

Block Scope

By blocks, we mean everything between curly braces, such as the block of an if statement or a for loop. So just like with functions, variables declared inside a block are only accessible inside that block and not outside of it.

The big difference is that block scopes only apply to variables declared with let or const. That's why we say that let and const variables are block scoped.

This means that if we declared a variable using var in the above if block, that variable would still be accessible outside of the block and scoped to the current function or the global scope. And so we say that var is function scoped.

In ES5 and before, we only had global scope and function scope. That's why ES5 variables declared with var only care about functions and not blocks. They simply ignore them.

Finally, starting in ES6, all functions are now also block-scoped, at least in strict mode, which you should always be using. Just like with let and const variables, this means that functions declared inside a block are only accessible inside that block. We will see examples of all that in the next lecture.

To recap, let and const variables, as well as functions, are block-scoped. If you already know other programming languages, block scoping is probably more aligned with what you already know. Function scopes are weird for some beginners in the JavaScript world, and that's why block scopes were introduced in ES6.

To understand all this a little bit better, let's look at a more real and detailed example and learn about the scope chain. Let's consider the below code with different functions and blocks. We're going to take a look at the scopes in this code and build the scope chain.

Let's start with the global scope.

Scope Example Global

As you can see, the user variable is the only variable declaration that we have in the global scope. Technically, the first function also counts as a variable present in the global scope. But since I want us to keep it simple for now, we will only consider variable declarations and not functions.

Just keep in mind that whatever I'm explaining here for variables also works the same for functions.

Moving on, inside the global scope, we have a scope for the first function because, as we now know, each function creates its own scope. In this scope, we have an age variable declared right at the top of the function.

Scope Example First

Next, inside the first scope, let's now consider the second function, which will also create its own scope containing the job variable set to 'Software Engineer'.

Scope Example Second

As you can see, we have a nested structure of scopes with one scope inside the other.

Now comes the interesting part. In the second function, we have the below line of code where we need the user variable and the age variable, which were both not declared inside the current scope.

script.js
console.log(`${user} is a ${age} years old ${job}`);

We need these variables; otherwise, we won't create that string. Now the question is, how can this be fixed? How will the JavaScript engine know the values of these variables?

Well, the secret is that every scope always has access to all the variables from all its outer scopes, i.e., from all its parent scopes.

Scope Inherits Variables

From our example, this means that the second scope can access the age variable from the scope of the first function. This also means that the first scope can access variables that are in the global scope because that is the parent scope.

Scope Inheritance

Consequently, the second scope will then also be able to access the user variable from the global scope because it has access to the variables from the first scope.

Scope Inheritance Complete

All this also applies to function arguments. In this example, we don't have any.

This is essentially how the scope chain works. In other words, if one scope needs to use a certain variable but cannot find it in the current scope, it will look up in the scope chain and see if it can find a variable in one of the parent scopes. If it can, it will then use that variable. If it can't, then there will be an error. This process is called variable lookup.

Variable Lookup

It's important to note that these variables are not copied from one scope to another. Instead, scopes lookup in the scope chain until they find a variable they need, then use it. What's also extremely important to note is that this does not work the other way around. A certain scope will never have access to the variables of an inner scope.

Variable Lookup Direction

In this example, the first scope, for example, will never get access to the job variable that is stored in the second scope. One scope can only look up in a scope chain, but it cannot look down. In other words, only parent scope can be used and not child scopes.

With all this in place, the below line of code can be executed and log to the console Mark is a 30 years old Software Engineer, even though the user and age variables were not defined in the current scope. All the engine did was to get them from the scope chain.

We still have one more scope left here, which is the one created by the if block.

Scope IF Block

Remember that starting with ES6, not only functions create scopes, but also blocks. However, these scopes only work for the ES6 variable types. That is, for let and const variables. That's why the only variable that's in the scope is the decade variable.

Scope IF Block Variable

The millennial variable isn't declared with const or let, and therefore it is not scoped to just this block. Instead, the millennial variable is part of the first function scope.

Scope IF Block First Recieves

Again, for a variable declared with var, block scopes don't apply at all. They are functions scoped, not block-scoped. On the other hand, let and const are block-scoped.

This is one of the fundamental things you need to keep in mind about let, const, var, and scoping in general. So if you're taking notes, and I hope you are taking lots of notes, then this must be in there.

Now about the scope chain, if the millennial variable is in the first function scope, then, of course, the second function scope also has access to it, even if it doesn't need that variable.

Scope IF Block Second Recieves

Also, the scope chain does, of course, apply to block scopes as well. Therefore in our if block scope, we get access to all the variables from all its outer scopes. That is, from the first function scope and the global scope.

Scope IF Block with All Variables

That's why we said above that variables in a global scope are accessible from everywhere. They are because they are always at the top of the scope chain. We call variables in the global scope global variables.

Now it's important to understand that our if block scope does not get access to any variables from the second function scope and the same, the other way around. That's because of lexical scoping.

Scope IF and Second

The way we can access variables depends on where the scope is placed, i.e., where it is written in the code. In this case, none of these two scopes is written inside of one another.

They're both child scopes of the first function. We could even say that they are sibling scopes. And so, by the rules of lexical scoping, they cannot have access to each other's variables simply because one is not written inside the other one. We can also say that the scope chain only works upwards, not sideways.

I hope everything still keeps making sense at this point. If not, don't worry we will see all this working in practice in the next lecture.

Let's now talk about the differences between the scope chain and the call stack. Together we are going to see how the call stack, execution context, variable environments and scope are all related to one another.

Once more, let's consider the below code.

script.js
const a = 'Mathew';

first();

function first() {
  const b = 'Hello!';
  second();

  function second() {
    const c = 'Hi!';
    third();
  }
}

function third() {
  const d = 'Hey!';
  console.log(d + c + b + a);
  // ReferenceError
}

We have three functions called first, second, and third to make this easier to understand. We start by calling the first function, which then calls the second function, which in turn calls the third function.

From what we learned before, the call stack for this example will look like this.

Scope and Call Stack

One execution context for each function in the exact order in which they were called. I also included the variable environment of each execution context.

For now, all this has nothing to do with scopes or the scope chain. All we've done up till now is creating one execution context for each function call and filling it with the variables of that function.

Now that we have all these variable environments in place, we can start building the scope chain. As always, we're going to start with the global scope. The variables available in the global scope are exactly the ones stored in the variable environment of the global execution context.

Scope and Call - Global Scope

Note that I'm actually including functions in each scope in this example, unlike we did in the previous examples. Now in the global scope, we also call the first function, which is the reason why we have an execution context for it in the call stack.

This function also gets its scope, which contains all the variables declared inside the function. Once again, this is exactly the same as the variable environment of the function's execution context.

Scope and Call - First Scope

However, that's not all. Thanks to the scope chain, the first scope also gets access to all the variables from its parent scope.

Scope and Call - First Scope Chain

Now, as we already know, the scope chain is all about the order in which functions are written in the code. But what's really important to note here is that the scope chain has nothing to do with the order in which functions were called. In other words, the scope chain has nothing to do with the order of the execution contexts in the call stack. The scope chain does get the variable environments from the execution context as shown by the red arrows above, but that's it. The order of function calls is not relevant to the scope chain at all.

Now, moving on to the second function now, once again, its scope is equal to its variable environment. Also, it's lexically written within the first function; therefore, it will have access to all its parent scopes as well.

Scope and Call - Second Scope Chain

So we can say that the scope chain in a certain scope is equal to adding together all the variable environments of all the parent scopes.

In the second function, we try to call the third function. You may certainly ask yourself, but why does that work? Well, it works because the third function is in the scope chain of the second function scope as we can see here in our scope chain diagram.

Scope and Call - Second Scope - Third FUnction

It's a function in the global scope or a global function, and therefore it's accessible everywhere. Of course, this will create a new scope along with the scope chain, as we already know.

Scope and Call - Third Scope Chain

Great, so what happens in this third function? Well, we're trying to act as variables d, c, b and a. As you can see, d is no problem because it's right there in the third function scope. The variable c is not in the local scope, so JavaScript needs to do a variable lookup. So, it looks up in the scope chain looking for variable c, but it's not there because c is defined in the second function, and there is just no way in which the third function can access variables defined in the second function.

That is true, even though the second function called the third. And so here is even more proof that the order in which functions are called does not affect the scope chain at all. As a result, we get the reference error because both c and b cannot be found in the third scope nor in the scope chain.

With this, I hope I made it clear that execution context, variable environments, the call stack, scope, and the scope chain are all different but still very related concepts. If it's not yet clear, then try to read the lecture over again or later.

I know that this was quite a long lecture and I guess the longest lecture since the begining of this course with a lot of stuff to take in. Here is a handy summary of the main takeaways from this lecture.

Summary Scope and Scope Chain

This is in a nutshell, scoping in JavaScript. See you in the next lecture.