The Ultimate Guide to Execution Contexts, Hoisting, Scopes, and Closures in JavaScript

Video Statistics and Information

Video
Captions Word Cloud
Reddit Comments
Captions
♪ [music] ♪ [00:00:09] You know, it may be surprising, but in my opinion, the most important concept and feature of the JavaScript language to understand is execution context. The reason for that is because if you have a good understanding of the JavaScript execution context, you'll have a much easier time understanding some more advanced topics like hoisting, scopes, scope chains, and closures. So with that in mind, what exactly is an execution context? Well, to better understand it, let's first take a look at how we write software. One strategy for writing software is to break our code up into separate pieces. These pieces have a few different names. We can call them "functions," "modules," "packages," but they all exist for a single purpose – to break apart and manage the complexity in our applications. So here, what we have on this left side is some code. And then, on this right side, what I've done is I've broken this code into different pieces. In this case, I've broken them into functions. So we can see here that the right side is a little bit easier to read and to comprehend than this left side, only by breaking some of the functionality up into different functions. Now, instead of thinking like someone authoring the code, instead of thinking like someone writing the code, think in terms of the JavaScript engine, whose job it is to interpret code. Can we use this same strategy of separating code into pieces to manage the complexity of interpreting code, just like we did in order to write it? It turns out we can, and these pieces are called "execution contexts." So just like functions or modules or packages allow you to manage the complexity of writing code, execution contexts allow the JavaScript engine to manage the complexity of interpreting and running your code. So what I've done in order to allow us to see the different execution contexts that it created is I've created this tool called "JavaScript Visualizer," which as it says here is "a tool for visualizing execution context, hoisting, closures, and scopes in JavaScript." So the first execution context that gets created when the JavaScript engine runs your code is called the "global execution context." And you'll notice here, even without any code over here on the left, we still get a global execution context. Initially, this execution context will consist of two things, as you can see here. We have a global object, which in the browser is the window object. If you're running JavaScript in a Node environment, it's going to be called "global." And then we also have a this object, which in the global execution context just points to the window object. So again, even if your program doesn't have any code inside of it, the JavaScript engine is still going to create a global execution context. And inside of that, we'll have two properties – window, which will just point to the global object, and this, which will point to the window object. So now, let's actually add some code to our program, so we can see how the global execution context will change when we have some code. So what I'm going to do is create a few variables here. We'll say, "name" and "handle," and then I'm going to create a function declaration that's called "getUser," which will return us an object. We'll just say, "name: name," and then handle is going to be "handle." All right. So the first step we take, you'll notice a few different things over here. The first thing is we are in the Creation Phase. Each execution context is going to have two phases – the Creation Phase and then, later on, a phase that's the Execution Phase. So in the Creation Phase, what happens is you'll notice that we have our window object here. We have that global object that's created for us. We also have the this object that's created for us. And these two are going to happen no matter what, as we saw earlier when we didn't have any code. And then, the next thing that happens is all of the variable declarations are assigned a value of "undefined," and the function declaration is put entirely into memory. So in the Creation Phase of the global execution context, four things happen – we create a global object, we create an object called "this," we set up memory space for variables and functions, and then we assign variable declarations a default value of "undefined," while placing any functions directly in memory, as we see here. So then, when we take another step, the Creation Phase is now over, and now it's time for the Execution Phase. This is the phase where JavaScript starts executing your code line by line. So you'll see here, we have a variable declaration. As we step through this, now, after Line 1 has run, in memory, instead of name being undefined, name is now "Tyler." So we can keep doing this. Handle is now going to be the string, "@tylermcginnis," and then our program is over because it has finished executing. So now, as a little test, what I'm going to do is open up the console, and then we are going to run this code. And the question I have is, what are these lines going to log? You'll notice here that we are console logging before we declare any of the variables here. So based on what you know about the global execution context and its two phases, the Creation Phase, as well as the Execution Phase, what are we going to get in the console? Well, if we run this, you'll notice that we get name is undefined, handle is also undefined, and then getUser is just referencing the function in memory. And again, the reason for that is because, as soon as we're in the Creation Phase, all of the variable declarations are assigned a default value of "undefined," and the function declaration is put into memory. So then, when we enter the Execution Phase and we start running these lines, at this moment, name is undefined, so we get "undefined." Handle is undefined, so we get "undefined." And getUser is just sitting in memory because it's a function declaration, so we get the function itself. Now, this brings us to one of the very first topics that sometimes is seen as being a little bit advanced. But now that we have a good understanding of the global execution context, it's pretty straightforward. So remember when, during the Creation Phase of the execution context, we said that any variable declarations are assigned a default value of "undefined?" Well, that term in JavaScript is called "hoisting." The thing that's confusing about hoisting is that nothing is actually hoisted or moved around. All hoisting is, is it's the process of assigning a variable declaration a default value of "undefined" during the Creation Phase. That's really all it is. So at this point, you should be fairly comfortable with the global execution context and its two phases – the Creation Phase, as well as the Execution Phase. And the good news is there's only one other execution context you need to learn, and it's almost exactly identical to the global execution context. And it's called the "function execution context," and it's created whenever a function is invoked. So this is important. The only time an execution context is created is, first, when the JavaScript engine starts interpreting your code, that's the global execution context, and then, after that, whenever a function is invoked. So now, the main thing we need to figure out is, what's the difference between the global execution context and the function execution context? So if you remember, we had a list of everything that the Creation Phase in the global execution context did. It was this right here. We said, "It creates a global object, It creates an object called "this," sets up memory space for variables and functions, and then it assigns variable declarations a default value of "undefined," while placing any function declarations in memory, a.k.a. "hoisting." So can you think of anything on this list that makes sense for when the global execution context is created, but doesn't necessarily make sense for when a function execution context is created? It's probably this one right here. It makes sense to create a global object the very first time the global execution context is created, which is only one time in your entire program. But it doesn't make sense to make a global object whenever a function is invoked. So the only difference between the global execution context and a function execution context is, instead of creating a global variable, a function execution context is going to create an arguments object. Right? Because when you invoke a function, that function can receive any arguments. And if you're familiar with JavaScript, you'll know that we could do something like this, where if we log "arguments," arguments is going to be an array-like object inside of this function. So "arguments" is literally a keyword in JavaScript that is an array-like object for all of the arguments that you are passing to the function. So we can actually see this ourselves. Let's go ahead and go back to the code that we had earlier. And now, instead of just defining the function, let's actually go ahead and invoke it as well, and we'll see what happens. So notice, first, we're in the global execution context in the Creation Phase, so hoisting is happening here. We define, or we set all of those variable declarations to "undefined," and then we start executing the program as we saw earlier. But now, when we invoke this function, notice that we have a brand-new execution context here. And inside of that, before we ever start executing the code, a.k.a. "when we are in the Creation Phase," we already have two things. We have an arguments object. In this case, it's just empty, so it has a length of 0. And then we have a this object, which in this case also points to the window. So just as we saw in the Creation Phase of the global execution context, two things were created for us. Except this time, instead of a window object, we have an arguments object, and then we still have this. And because getUser doesn't have any variables, the JavaScript engine doesn't need to set up any memory space or hoist any of those variable declarations. So once we finish stepping through this, you'll also notice that after getUser is finished executing, notice it's removed from the UI over here. And this brings us to the topic of an execution stack. So the way it works is any time a function is invoked, a new execution context is created and added to the execution stack. Whenever a function is finished running through both the Creation Phase and the Execution Phase, it gets popped off the execution stack as we just barely saw. Because JavaScript is single-threaded, meaning only one task can be executed at a time, this is pretty simple to visualize. So what I've done is created three different functions – a, b, and c. They're all nested inside of one another. So first, we are on the global execution context, and then we go into the execution context of the a function. Let's open up the console here, so that we can see this as well. So then, 'In fn a' is going to be logged. And then, when b is invoked, we get a new execution context on the execution stack. It'll log 'In fn b'. Then, when c is invoked, we get another one. So now, we're in function c. And then, once c is finished executing, the execution context is removed from the execution stack. Same thing for b, and then same thing for a. So at this point, we've seen how function invocations create their own execution context, which get placed on the execution stack. But what we haven't talked about yet is how local variables play into that. So let's change up our code just a little bit so that our functions now have some local variables. So here, our code is pretty much the same. But now, we have this local variable, "twitterURL," inside of our getURL function that we just barely created. And so now, let's see how JavaScript Visualizer is going to visualize this. So as we step through this, notice the same thing – Creation Phase, global execution context, all of the variables are undefined. And then, eventually, we get into the execution context of getURL. And you'll notice in the Creation Phase of the getURL execution context, we have arguments, which we've talked about. We have this, which we've also talked about. But now, "twitterURL," which is the local variable inside of the getURL function, is hoisted to a value, or assigned a default value of "undefined," just as our variable declarations up here were as well. But one other thing is that you'll notice we passed in "handle" as an argument to getURL. We can see that on the arguments object right here. Any time you pass a variable to a function, that variable during the Creation Phase is going to be put on or put in the variable environment of the current execution context. So by passing in a variable called "handle," it's as if we created a local variable called "handle" here and set it equal to whatever it was. In this case, it was "@tylermcginnis." So we can visualize that nicely here, because we have arguments, this, any arguments we passed in are now kind of like local variables, and then we have…Because hoisting any variable declarations inside the function are assigned a default value of "undefined." So now, once we go into the Execution Phase, "twitterURL" becomes that URL, and then we return "twitterURL + handle." And then, getURL gets popped off the execution stack, and then our program is finished executing. This brings us to the topic called "scope." In the past, you probably heard a definition of "scope" along the lines of "where variables are accessible." Now, regardless of whether or not that actually made sense at the time, with your newfound knowledge of execution context and our JavaScript Visualizer tool, hopefully, scopes will make a lot more sense. So if we look at the definition of "scope" on MDN, you'll notice that it says, "The current context of execution." That should sound familiar. We can think of scope, or where variables are accessible, in a very similar way to how we've been thinking about execution context. So here's a test for you. Let's say I had a function called "foo" inside of here, we had a local variable called "bar," and then I invoked foo and then did console.log(bar). What is going to be logged to the console when this runs? Well, let's look. So we step in, create a new execution context for foo. That creates a new variable called "bar." But then, once the execution context is popped off the stack, when we go to console.log(bar), looking at JavaScript Visualizer, it makes a lot of sense that we are going to get "ReferenceError: bar is not defined." By the time this line runs, foo's execution context where "bar" is defined has already been popped off the stack. So it's as if "bar" was never even a thing because foo's execution context is already gone. So let's look at another example here. So before we run this, let's walk through it. We have a function called "first," where we define a name, "Jordyn," and then console.log it. Second does the exact same thing with "Jake." We console.log(name) here, set a local variable or, in this case, it's a global variable, name to "Tyler," call "first," call "second," and then console.log(name). So go ahead and hit Pause on this video right now. Walk through this, and then think about what's going to be logged to the console. So first, we enter the global execution context in the Creation Phase. The variable declarations are put into memory, and the name variable is assigned a default value of "undefined." So then, when we get to this line right here, "console.log(name)," what's gonna happen is, at this moment, name is undefined, as we can see, so we get "undefined." We then set name to "Tyler," and then we enter the first execution context, or we create a new execution context called "first." In the Creation Phase, we have arguments, this, and then a name which is default as "undefined." We assign name to "Jordyn." And then, when we console.log, we get "name." So the way this works, and this might be obvious, is when you try to access a variable in an execution context, it's first going to look to its own execution context to see if that variable exists. In this case, it does. So it just logs "Jordyn." So then, first is taken off the execution stack. Then, we go into second, same thing, name gets set to "Jake." And then, same logic, because name is in the current context, it's going to log "Jake." And then, at this moment, we console.log(name), and there is a name variable here in the global execution context variable environment, so we get "Tyler." So now, what if the variable doesn't exist in the current execution context? Will the JavaScript engine just stop trying to look for that variable? So let's see an example here. So let's set name to "Tyler." We can log the name, and then we invoke logName. So we have a variable named "Tyler." We go into the logName execution context. At this moment, logName doesn't have a local variable called "Name." So what it's going to do is it's going to look to its closest parent execution context to see if a name property exists. So what will happen is it'll say, "Hey, I don't have 'name,' but it looks like the execution context above me does. So go ahead and log that instead." And this is really the idea of scopes and scope chains in JavaScript. So if the JavaScript engine, like I said, can't find a variable local to the function's execution context, as we saw here, it'll look to the nearest parent execution context for that variable. That lookup process will continue all the way up, until the engine reaches the global execution context. In that case, if the global execution context doesn't have the variable, then it'll just throw a ReferenceError, as we saw earlier, because that variable doesn't exist anywhere up the scope chain, or the execution context chain. So at this point, we've talked about execution contexts and their phases – the Creation Phase, as well as the Execution Phase. We've talked about hoisting, we've talked about scopes, and we've talked about the scope chain lookup. There's one last topic I want to discuss and, historically in JavaScript, it's taken as a very advanced topic. But I'm hoping now with your knowledge of all of the topics that we just barely covered, it should be pretty straightforward. So what we're going to do is we're going to visualize this code right here. So notice, we have a count. We have a makeAdder function, which takes in a single argument, and what it returns is a new function that takes in a single argument. And then, when this inner function is invoked, it's going to return us whatever the initial argument was, plus whatever the new argument was. So what this looks like…And again, your brain might be getting a little weird right now. We'll walk through this. So what happens is makeAdder returns us a function, so add5 is going to be a function. So then we say, "count += add5(2)," which will give us "5" because that was the initial argument plus 2. All right. That was a lot. Let's visualize this, so we can see what happens. So we are in the Creation Phase. We define some variables, and then we enter makeAdder's execution context. At this moment, in the Creation Phase, we have arguments, this, then we have a local variable because that's what was passed in as an argument. So we enter the Execution Phase, and then makeAdder is going to be removed from the execution stack because it is going to be finished executing. But notice what happens here. We have this brand-new thing called a "closure scope." The reason we have this is because we had a function nested inside of another function. And whenever that happens, the inner function is going to create what's called a "closure" over the outer function's execution context. So you'll notice here that the variables inside of this closure execution context are the exact same as the variables that were in the makeAdder execution context. Again, the reason for that is because, later on when inner is invoked, it needs to have access to those same variables, that same variable environment. If it's still fuzzy, let's continue to step through this. So eventually, what happens is we invoke add5. That creates a new execution context on the execution stack, and then let's just read this as normal. So as we're executing, we get to this line right here, where we are returning "x + y." JavaScript is going to say, "Okay. Does inner have a y variable in its variable environment?" It does. It's set to "2," but we also need to know what x is. Does inner have an x variable in its variable environment? It doesn't. So just as we've done in the past, we want to look up to the closest parent execution context to see if that variable exists. We look up in the closure scope and it says, "Hey, does closure scope have a variable in its variable environment of x?" It does, and it's value is "5." So go ahead and return "5 + 2." So then, we continue stepping through this. And then, count gets changed to "7" because, as we saw, that was 5 + 2, and then the closure scope is removed from the execution stack. So let's visualize this just one more time. The takeaway here is, if you have a function inside of another function, as we do right here – we have inner inside of makeAdder – the inner function is going to create what's called a "closure" over the execution context variable environment of the parent function. So in this case, even though the makeAdder execution context gets removed from the stack, because we have these nested functions, inner creates what's called a "closure scope" over, again, the execution context variable environment of the parent function. So now, later on, when that inner function is invoked, as we see here, with the normal scope chain lookup rules, the inner function has access to any of the variables declared in the parent function's execution context, even though that parent function's execution context has been removed from the stack. And that whole concept, as you probably guessed by now, is called a "closure. So one last time, let's go ahead and do a real quick recap. When the JavaScript program starts to run, it creates a global execution context. This execution context has two phases. The Creation Phase – in the Creation Phase, four different things happen. First, in the global execution context, JavaScript will create a global object. We will always create a this object. We set aside memory space for any variables, as well as functions. Variable declarations get assigned a default value of "undefined," that's called "hoisting," and functions themselves get placed entirely into memory. The next type of execution context is the function execution context, which will happen whenever a function is invoked, as we see here. So the function execution context is the exact same as the global execution context. Except for instead of creating a global object, we create an arguments object, and any arguments that are passed into that function get added as if they were just local variables to the execution context. Then, whenever you have a function inside of another function, even if the parent function's execution context is removed from the stack, the inner function will still retain access to the variable environment of the parent function's execution context. This is called a "closure" that we can see here. And then, the last thing is the scope chain. At this moment, right here, JavaScript is going to look inside of the current execution context to see if the variable "x" exists. It doesn't, so then it will go up the scope chain to the next closest parent execution context. In this case, it's the closure scope. It will look for that variable. In this case, it exists. It's "5." So then, x will become "5." Y exists locally in the execution context, so y will be "2." So that will return "7," which gets added to count, which then changes to "7."
Info
Channel: uidotdev
Views: 48,434
Rating: 4.9529409 out of 5
Keywords: javascript, closures, scopes, hoisting, execution context, tyler mcginnis, tylermcginnis, ui, ui.dev, javascript hoisting, javascript scopes, javascript closures, javascript closures vs scopes, javascript hoisting scopes closures, javascript review, javascript tutorial, javascript for beginners, js, learn javascript, scope chain, scope chain javascript
Id: Nt-qa_LlUH0
Channel Id: undefined
Length: 25min 35sec (1535 seconds)
Published: Tue Oct 09 2018
Related Videos
Note
Please note that this website is currently a work in progress! Lots of interesting data and statistics to come.