Javascript Scope Chain and Execution Context simplified
The goal of this article is to explain the concept of scope chaining, execution contexts and the relation between them in simple terms. By the end of this article, you should be able to answer the following questions.
- What are Scopes and Execution contexts and what is the relation between them?
- How are scopes and execution contexts created?
- How is the scope chain created?
- How compiler uses scope chain?
- What happens in the background when we use a variable in our code?
- How compiler looks up the value of variables that we use in our code?
Did it ever happen to you that you encountered variable not defined/undefined errors in your code although everything looks good to you? We will discuss the reasons behind this vary issue in this article.
What the heck is a scope anyway?
“Scope in Javascript is what a context is in English. Just like context gives meaning to a word, scope gives meaning to a variable/object. In technical terms, A scope is just a finite set of variables/objects that an execution context have access to”.
Every function/execution context has its own scope. There is also a global scope that resides on top of every scope. Let’s get towards the code to understand further. In the following code,You will see how the JS compiler creates an individual scope of every execution context/function.
Code snippet 1.0:
const bestAvenger = 'Iron man';function a () {
const bestActor = "Neymar";
console.log(bestAvenger); // output:Iron man function c() {
const bestProgrammingLanguage = 'Html';
console.log(bestActor); // output:Neymar
b();
} c();
}function b() {
console.log(bestProgrammingLanguage); // not defined error
}a();
Let’s assume the scope of :
- global window is called G
- function a is called A
- function b is called B
- function c is called C
Now we can determine the individual scope (a set of available resources) of each function by looking at what lies physically where in the code:
G = { a(), b(), const bestAvenger }
A = { c(), const bestActor }
B = { }
C = { const bestProgrammingLanguage }
Something to remember : As you have noticed above , the functions lie in the scope where they are defined rather than where they are invoked.
Execution Context
Although we will be covering all the basics, there is still a lot to read about it that is not covered here.
Informal Definition :
The execution context is the environment of a function where it’s code is executed. Every function has its own execution context.
The execution context/environment of a function roughly equates to:
- Arguments sent to that function
- The scope chain of that function i.e. variables and objects available for the function to use (we are going to learn more about scope chain next)
- value of “this”
A Deeper Dive into Execution Contexts
Execution context works as a unit in the overall flow of execution. Since every function has its own execution context, the Js compiler maintains a stack of execution contexts. It tracks whether the execution contexts are in the correct order. The top of the stack contains the execution context of the function that is currently being executed.
The compiler pushes and pops the contexts according to the code flow.
Following diagram shows how the browser would maintain the stack of execution contexts for code snippet 1.0:
Execution contexts will pop out of the stack in LIFO (Last in first out) manner, when the execution is finished.
Now you can clearly understand what scope of an execution context means. You might want to re-read the definition of scope above
What is the scope chain and how it is created?
Every scope is always connected to one or more scope in their back forming a chain or a hierarchy, except the global scope. The global scope does not have any parent, and that makes sense too since it is sitting at the top of the hierarchy.
Now adding some variable usage in code snippet 1.0 to see how scope chain is created and how variables are looked up by the compiler.
code snippet 1.1:
const bestAvenger = "Iron man";function a() {
const bestActor = "Neymar";
console.log(bestAvenger); // output:Iron man
function c() {
const bestProgrammingLanguage = "Html";
console.log(bestActor); // output:Neymar
b();
} c();
}function b() {
console.log(bestProgrammingLanguage); //**not defined error**
}a();
Oops! There is an error, and it says bestProgrammingLanguage
is not defined. Lets find a logical explanation for this error.
As mentioned above, the scope chain is always created lexically. The parents of a scope are defined by where the execution context(function) lie lexically or physically in the code.
The Scope chain for code snippet 1.0 and 1.1 would be like this:
LEXICAL ENVIRONMENT SCOPE CHAIN
G lies in global, G=G
A lies in G, A=A+G
B lies in G, B=B+G
C lies in A & A lies in G C=C+A => C+A+G
How is this scope chain used by the compiler?
Whenever the compiler encounters a variable or an object, It traverses the whole scope chain of the current execution context looking for it. if it is NOT found there, It traverses the prototype chain, if it is NOT found there too, it throws not defined error.
In the above code the variables were accessed thrice.
- when
console.log(bestAvenger)
was executed, current execution context was A, let’s see what’s the scope chain of A is:
A=A+G. The compiler found the variable in G - when
console.log(bestActor);
was executed; current execution context was C, let’s see what’s the scope chain of C is:
C=C+A +G . The compiler found the variable in A - when
console.log(bestProgrammingLanguage)
was executed; current execution context was B, let’s see what’s the scope chain of B is:
B=B+G. The compiler could neither find the variable in B or G so it threw not defined error.
Time for a small test!
To end this article, I am writing a code problem which I expect that after reading this article everyone can solve, or maybe not ;)
What statements would be printed on the console for this code snippet:
const myNumber = '3';(function (callback) {
console.log(myNumber);
const myText = 'hello';
callback();
})(function () {
console.log(myNumber);
console.log(myText);
})
Conclusion
Javascript is single-threaded. There is always only one execution context being executed at a time. Thus, it is essential to understand how Javascript compiler handles the resources inside. We can conclude the following points from this article
- Every function has its own execution context
- The execution context is the environment of a function which includes its own scope.
- The compiler creates the scope of a function by looking at where it is placed in the code.
- The compiler creates a hierarchy of scopes called scope-chain, Global scope sits at the top of this hierarchy
- The compiler looks at the scope-chain backward when a variable is used in the code, if it is not found, It throws not defined error.
Further Readings
There is a blog series that I personally like the most, written by Alexander Zlatkov co-founder SessionStack. It covers all the core details of how javascript works behind the scene.