DEV Community

Allen Shin
Allen Shin

Posted on

Understanding Execution Context in JS

It has been about 2 months since I've graduated from FlatIron School (I was lucky enough to finish right before everything close down for COVID-19), and I've been continuing to focus on studying since I've finished. I'm taking this time, because I felt like I had a lot of holes in my understanding. FlatIron was truly a great beginner's class, giving students an understanding of coding that was analogous to the real world. The problem with analogies, though, is that there are details that are lost in translation, especially with something as unique as a computer program.

Ever since I started learning to code, I felt as if I was accumulating a bunch of tools to match with specific problems, without understanding the physics of what I was building. Even if I continued to find the solution through documentation, every article simply felt like the matching block that fit into a specific problem. Just plug and chug. It turns into the common warning most programmers give to not just copy someone else's code as your own solution.

There are a number of resources (all are for introductory, but slightly different topics), which I have used to improve my understanding of the basics. These include Harvard's CS50 class (a course to cover a variety of foundational computer concepts and languages), as well as the Udemy course Javascript: Understanding the Weird Parts by Anthony Alicea, both of which I highly recommend for any aspiring Javascript developer. Today I want to share information about lexical environments in the Javascript language, which I have learned from the Udemy resource.

In Javacript, it's important to remember that what is happening under the hood is not magic, even though at times all we have visualize code is our imagination, which sometimes does lead to magic.

To start, we'll talk a little bit about the two phases that exist when a Javascript program is first run: the creation phase, and the execution phase.

During the creation phase, the scope, or the lexical environment for the variables in the code are created. Lexical environment simply means the physical environment which the variables exist, which can also be called the execution context. The idea is scope is an idea that is referring to the lexical environment in the context of the accessibility of variables. For the purposes of our discussion, we will use the word execution context to describe the object which defines the scope. Whenever an execution context is created, 3 objects are made which are the global object, 'this', and outer environment.

The global object is the place where every single variable and function is stored as memory. The 'this' object is a self-referential object which you can use to refer to the global object itself in the execution of the code, and the outer environment refers to the execution contexts outside the one currently executing.

When the creation phase begins, it creates memory space for all the variables and functions that exist at the global level. This global execution context is created first, because it is the currently 'executing' portion of the code. That's why I thought it was useful to use the word execution context, rather than simply using the word lexical environment, even though they are technically identical; the physical environment for the variables are created when that code for that environment is being executed. At this level, the code has still yet to run through the execution phase, so what we will get at this stage is all your variables and functions with memory space but still left as undefined. Once you get to the actual execution phase, then the code runs through your code again to see what to define the variables, as well as what to do with them.

To look at a basic example:

var a 
a = "Hello world"
console.log(a)
Enter fullscreen mode Exit fullscreen mode

Line 1 is the only thing that happens in the initial creation phase, and then after that is run, the execution context defines the variable a on line 2 and now that it exists in the global object, can go ahead and console log it on line 3.

Now that we've looked at what happens at just the one global execution context, what if you were to execute other functions? Well as we said before, whichever code is currently being executed, there is another execution context that is made for that code, and then sent to the call stack to be executed. I will cover more details about the call stack in later examples in the article, but for now, all we need to know is that when a function is sent to the call stack after being executed, it's execution context is created.

To keep things simple let's say we wanted to do the same thing as before, but we wanted to make a function to do the console log. This would require for us to call the function.

var a = "Hello World"
function b(){
  var c = "some string"
  console.log(a)
}
b()
Enter fullscreen mode Exit fullscreen mode

For the creation phase, you get the same saving of the first variable to memory space like before, but this time we're also going to include the function, as well. After the creation phase is completed, the code then executes as before defining the a variable as the string "Hello World", and then it executes the b function. The b is then moved on top of the call stack and it begins it's own creation phase. For this particular example, in the first line I included the variable c to indicate that this variable is allocated to memory space during the creation phase. Once that finishes, then we move onto the b function's execution phase where the variable c is defined and a is console logged.

To make a brief but important comment about scope, the variable a is recognized here because when the b function's creation phase started, you had the outer environment object created alongside the object for the storing of variables within the b function's execution context. This outer environment object is where it took into account that the global execution context already defined the variable a as "Hello World". This is exactly scope works the way it does. The outside execution context has no 'inner environment' object to indicate the variable, while the inner scope does have an 'outer environment' to check what has already been defined in the outer environment.

Let's increase the difficulty a little. What do you think would happen if you executed this piece of code? What would a be console logged as?

var a = "Hello World"
function b() {
  var a = "Goodbye World"
  c()
}

function c() {
  var a = "Maybe World"
  console.log(a)
}
b()
console.log(a)
Enter fullscreen mode Exit fullscreen mode

This was the most effective example in helping solidify this concept of execution contexts.

Now if you went and tried to run this in your code, you got the result "Maybe World", then "Hello World". For me this wasn't what I expected given my previous understanding of scope. For me, scope was just the ability for a variable to look outside of its own execution context to define the variable, so when there are multiple creations with different definitions, I had no idea how the system chose what to call a in this example.

However, armed with our new understanding of creation and execution contexts, it becomes clear why the 'a' variable in function c is console logged as "Maybe World" and the one in the global context is console logged as "Hello World". When this code runs, the variables and functions are all designated memory space during the initial creation phase pertaining to their own execution context. They are all variables with the name a but are different variables in different execution contexts. That is why when the execution context runs, and we define and console log the variable 'a', we are working with multiple 'a' variables. Each execution context has it's own 'a' variable and assigned different definitions. To keep track, currently there's one for the global context, one for the b function context, and one for the c function context.

Now you might be wondering the reason for why 'a' wasn't console logged first as Hello World, since the global context is the first to define the variable. This is where it is important to mention a bit about how the call stack works in Javascript. The way it works is described with the acronym LIFO, or Last in First Out. This means that if the function is the latest one to be executed, it doesn't move onto the rest of the code before it is 'popped off' of the call stack. This is why it is then also the first one out. When it is executed, the creation phase occurs where all variables are executed, and then during the execution phase, if another function is executed, its execution context is then created and run completely until it returns to finish the outer context execution phase work.

If you look at our example again, the variable 'a' and all the functions are allocated memory space. Then, the b function is executed and its execution context is created. Then we run into another function, and it's execution is created. The innermost function, c now has to run to completion before it allows b's execution context to finish, and then finally the global one, where after waiting all that time, the global 'a' variable can now be console logged as "Hello World".

Having run through this seemingly basic example of code line by line to explain how its execution context is created and run, I hope this understanding of how the Javascript engine runs, gives you a more thorough understanding of code and how to debug it. I know it did for me.

Top comments (1)

Collapse
 
mwong068 profile image
Megan

This article was super informative and definitely added to my understanding from what was taught in school. Thanks for this Allen!