Check out a free preview of the full Test Your JavaScript Knowledge course
The "Scope & Closure" Lesson is part of the full, Test Your JavaScript Knowledge course featured in this preview video. Here's what you'd learn in this lesson:
Lydia explains the concept of scope and closures in JavaScript. They discuss how closures work by referencing lexical environments and demonstrate examples of closures in code. The instructor also covers topics such as hoisting, variable declaration, and block scope.
Transcript from the "Scope & Closure" Lesson
[00:00:00]
>> Lydia: Scope & Closure, so I'm sure you're aware that we have the global scope, which is just for variables defined on the global, then we have block scope. So anything defined within the curly brackets. And we have function scope. And all this means, of course, is that variables inside of these scopes are available to other functions or any functionality within that scope.
[00:00:19]
And of course, if we, like in this case, we have a fruit variable defined within that function, so it's only available within the function scope, and using it outside of it will throw a reference error. So we cannot access variables that are further up the scope chain. Of course, moving it will make it available to us and the reference error will be gone.
[00:00:39]
But something that's a bit more interesting, I find, is closures, because this is something that can be kind of difficult to understand at first. Because here, it might look normal, but what we have here is we have a function outer, which returns a function inner. But within the inner function, we use a value that we pass to outer to actually create a sum like x+y.
[00:01:00]
But how does inner still know what we pass to outer if it's in a different scope? And this all depends on how JavaScript works with memory. So here we have the global lexical environment, and then we have the environment record. And the global environment record consists of both the object environment record and the declarative environment record.
[00:01:22]
The object one is for function declarations and var keywords and then the declarative one is for const, let, classes, everything else. So in this case we have the outer function which creates a function object. So this is just, we haven't called it yet, this is just like the function object, which contains a call method that we can use to actually invoke it.
[00:01:43]
But this is first created in memory like this. And outer also has an environment property. And this always points to the environment in which that function was created, which in this case is the global lexical environment. And this is something important to remember. Cuz now we have the out variable, which is const, so declarative environment record, which calls outer too.
[00:02:04]
And this creates outer's lexical environment, so that call method is invoked, which creates a lexical environment. And because we pass 2, it sets x to the argument to 2 in this lexical environment. Now in this case, we're not using any closures here cuz now outer is just returning x.
[00:02:22]
So we're setting out to whatever it returns to 2, and then the lexical environment is removed from memory cuz we don't use it anymore. But this is different when we use that closure, cuz now we still have the outer function object which we then call.
>> Lydia: So yeah, so we have the lexical environment, and within outer, it returns the inner function.
[00:02:46]
So this creates yet another function object, namely the inner function object. And now we're setting out, or the variable out, not equal to the value of x, but we're actually setting it equal to the function object that we just created. So the inner function object. And as you can see here, the inner function object also has the environment that still points to the lexical environment in which it was defined, which in this case is the outer lexical environment.
[00:03:11]
And because it still references that from the global scope, or like from the global lexical environment, it cannot be garbage collected. So whenever we use or whenever we use, like x+y, even though x isn't available in the inner function object, I'm just gonna call it now, cuz then you can see the environment record.
[00:03:27]
The environment record doesn't contain x. So it goes down to outer env, and then the environment, and there it finds x. So that's how it still knows what x is when we call inner, even though it doesn't have it in its own environment record. That's essentially what closures are.
[00:03:43]
It's just the fact that a lexical environment is still referencing environments from the outer lexical environment. I hope that makes some sense. It's just going down that environment chain pretty much. So that's what closures are. It's just the fact that it still references it from the global lexical environment, which is why it cannot be garbage collected.
[00:04:10]
So just keep this in mind. Because based on that, we have the following question, what gets logged? So we have an arrow function, outerFunc, that contains the count variable set to zero and it returns another function, that increments count. Then we have the counter variable, which is just whatever gets returned from outerFunc, and then we're calling the login counter twice, or the invocation of counter.
[00:04:38]
>> Lydia: So, is it either 0 and 1, 1 or 2, 1 and 1, or 0 and 0?
>> Lydia: The right answer is B, it's 1 and 2. So let's see how this works. So we have the counter variable that we set equal to the returned value of the outerFunc function, which is a function object, in this case it's just an anonymous function.
[00:05:02]
We didn't give it any any name or something. So here a counter is equal to the function object. And you can see that the environment of that function object points to the environment in which it was declared. So in this case, that is the outerFunc lexical environment. And this contains the environment record with count 0.
[00:05:21]
So now when we call it, which we do twice, we create the lexical environment for that anonymous function. And of course, this is returning ++count. It doesn't have its own count, so it goes down that environment chain until it finds it, which in this case is in the outerFunc lexical environment.
[00:05:37]
So there it increments it. So now count is one on the outer function lexical environment. Now the call ends, so it gets removed from memory, but then we call it again. It does the exact same thing. It still doesn't have count, so it just increments it on the outerFunc lexical environment.
[00:05:52]
So the first time that we log counter, you saw that we just incremented it. It's one we do ++count. So it logs it after incrementing it, so it's one, and then the second time we log it, it's again ++count. So ++1 is 2. So that's how it got 1 and 2, make sense?
[00:06:09]
>> Speaker 2: So it was count++, would it be then 0, 1?
>> Lydia: That would be 0, 1, cuz then, it first returns it, or first logs it, and then increments it, but now we do a ++count, good. Now then we go to the next question, question six. What gets logged?
[00:06:29]
So we have the createCounter function, which has a global count of 0. This also contains a function incrementCount. Which has the incremented value being ++ global count, and this returns the incremented value. And we have a variable counter, which is whatever gets returned from createCounter. We have incrementCount, and then we call createCounter, and then we increment count, so we're gonna be 0 1 0, 1 1 1, 0 1 2, 1 2 1, or 1 2 3?
[00:07:04]
>> Lydia: All right, so the right answer is 1 2 1, number D. All right, so essentially what we have here is we again in the object environment record. So where function declarations are we have the createCounter because it's a regular function, I kind of had to format the code block a bit different here.
[00:07:23]
But it's just the same as what we just saw, and then in the declarative environment record we have the counter, cuz we define it with const, and then createCounter. I just see that this actually should have pointed to another object like regular object that it then had the incrementCount property on it.
[00:07:38]
This won't really change any of the answer, but I'll make sure to update that. But anyway, so we set counter equal to the function object of incrementCount. And we call it, the very first console.log incrementCount. And the incrementedValue is ++globalCount, so it doesn't have its own globalCount. So it goes down that environment and it mutates globalCount on the createCounter lexical environment, because ++ actually mutates it.
[00:08:04]
But then it also has its own variable called incrementedValue, which is the value of the incrementedCount, so we now have the incrementedValue of 1 and globalCount of 1. Now this function stops running, we just logged it, gets removed from memory, and then we call it again, so another lexical environment gets created.
[00:08:22]
Hello, there. Now incrementedValue is 2, because that global count was already at 1, and it uses that same value, it went down the environment chain to find 1, we incremented ++ global count, which is 2. So in this lexical environment, the increment value is 2, global constant also 2.
[00:08:39]
But now, we call createCounter again, which means that we're calling that function again. So we're creating a new lexical environment, cuz this is a new function invocation. And in this case, globalCount is just the default, it's 0 again. So now when we call it, it's 1, cuz it's just that ++ 0, cuz globalCount was 0 by default, which is 1.
[00:08:59]
So on the third log, it just logs 1 again, cuz we're not pointing it to the same lexical environment as the previous two. Does it make sense? So a function of vocation creates a new lexical environment, and if that returns another function, it's a new function object in a new lexical environment.
[00:09:16]
Makes sense? Cool, yeah, as I said, what should have actually happened here is that, cuz I'm returning an object, I'm just saying that now that has the incrementCount as a property. So in between, or the counter should have actually pointed to an object, which then pointed to the function object.
[00:09:34]
But again, it doesn't change the output or how this works, so we should be good. Is this comparison true or false? So we have the createUserManager function. We have a user of null, then you have a function that sets a user to a new object, returns to user.
[00:09:53]
And then we have a createUser variable that is the return value of the createUserManager function. And then we say, okay, createUser John Doe is strictly equal to another createUser John Doe. Is it true or false?
>> Lydia: All right, and the answer to this is it's false. It's not equal, cuz what we see here is that we have createUser variable, which is equal to whatever gets returned from the createUserManager.
[00:10:21]
This didn't really fit on the screen, but that returned an anonymous function. And in the createUserManager, we have the letUser set to null, and the environment of that anonymous function object points to that lexical environment. So createUserManager lexical environment named John Doe, and within that function, we set user equal to a new object.
[00:10:44]
But since this is a new lexical environment, we just create a new object in memory, and that then gets returned. So createUser John Doe just returns to that user object that we just created in that createUserManager lexical environment. So that function, it returned something so it's gone out, and then we call it again for the other strict equal.
[00:11:05]
And again, we create a new user here, and it doesn't know that we already created another user object in the previous one. So it just creates a new function object in the createUserManager lexical environment. And objects, when we're comparing objects, we're actually comparing the references that they have in memory, and they both have a different reference in memory.
[00:11:28]
The first createUserFunction created a different object in memory than the second createUserInvocation, so that's why it's false. And that useStrict was just to throw you off. Cuz normally, when we have useStrict in functions or in the global scope, it'll say, you cannot use a variable that we haven't defined yet.
[00:11:48]
Like saying user in a global scope would say, first, you have to actually declare that variable before you're able to set anything equal to it.
>> Speaker 3: If we were to console log, createUser Jane Doe, and then create a John Doe and then createUser John Doe. The difference would be the time it's created, the created value will be different.
[00:12:11]
>> Lydia: No, it's actually a different object in memory. And when we're comparing objects, even saying object = object, this is false. Cuz when we're creating an object, this is a reference to a point in memory, and it's not the same reference. But when we did const object is equal, and then we're saying object = object, that is equal, cuz this is just one reference in memory.
[00:12:35]
But yeah, it's just creating a new object, so that's why we're essentially saying object = object, which is false.
>> Speaker 3: I asked that because on line 6, there's also createdAt value.
>> Lydia: Yeah, so it would have been, they would have had different values anyway. So it would have been false anyway, but even if it was the same.
[00:12:53]
>> Speaker 3: And if it were the same, it wouldn't matter.
>> Lydia: Yeah.
>> Speaker 3: Yeah, even if that value is not there, even if it's just user's name.
>> Lydia: Yeah, even this wouldn't be.
>> Speaker 3: Yeah, yeah.
>> Lydia: Yeah, cuz we're creating it new, yeah.
>> Lydia: All right.
>> Lydia: Leave this as the next one.
[00:13:16]
What gets logged? So it's pretty similar to the previous one, but easier, I think. This one, we have a createCounter function, which receives the initial count parameter. Then we have a counter variable, createCounter(10). We invoke counter twice and then log it the third time. Is it 1, 11, 13, not a number, or a reference error.
[00:13:38]
>> Lydia: All right, the right answer is C, 13. So in this case, it's pretty similar to what we had last time. We have the counter variable that we set equal to the function object that gets returned from createCounter. So in this case, it's that kind of anonymous function.
[00:13:53]
And then we have the count variable in the lexical environment in the environment record of the lexical environment of createCounter, so that's 10. And then we invoke counter twice, and we use count in that returned function, which doesn't have its own count variable. So it goes down that environment chain to find it and to create counter lexical environment.
[00:14:16]
So that increments it and sets it equal or that increments it by one. So we have 10, and then we have 11, and then we make it 11, sorry, and then we make it 12. And then on the last one, we actually log it, so not the first two, and then we increment it by one again, so that makes it 13.
[00:14:34]
So it keeps that same reference, it uses that same count variable within that create count the counter lexical environment, which is why it increments it based on or after every invocation. That make sense? [LAUGH] I should have probably made a little flow for this as well, but it's the same as we did last times, but a bit easier.
[00:14:53]
Okay, I think this is the last question about this topic. So which statements are correct? Hoisting is the process of moving functions and variables to the top of the file. Variables declared with let and const are hoisted. Variables declared with the var keyword are uninitialized. Hoisting occurs during the execution phase and import declarations are hoisted.
[00:15:18]
>> Lydia: The right answer here is B and E. So variables declared with let and const are hoisted and import declarations are hoisted as well. So let's see what that means. So whenever we run a script, we actually have two phases. First, we have the creation phase in which JavaScript sets up memory for everything that we have in our script.
[00:15:34]
So first, it creates the sum, it's a module lexical environment. I won't go too deep in the module stuff right now. But it does create that and we have the myFunc regular function. So in the object environment record that, and it immediately creates that function object for functions as well.
[00:15:54]
Now for variables fine with the var keyword, it also sets up memory with a default value of undefined Find but then we have the const and let which you might know are they are still hoisted meaning like we do set up memory space for them already but we just don't initialize them yet, like they don't have that default value of undefined.
[00:16:12]
So age username and also arrow function because it's still a const keyword are all uninitialized during the creation phase. And then, we have the actual, execution phase in which JavaScript goes one by one. So, okay age is 25, username is John, email is email.com, and then arrow function is that new function object.
[00:16:32]
So, this is why we cannot, access age and username before their declaration, because they are uninitialized, and then you get that reference era with IK, you cannot access this on an uninitialized value, but it does know that. All right, we do have age, it's just not initialized yet and that's the hoisting part.
[00:16:49]
So sometimes you'll see like, only variables with the var keyword are hoisted or only functions are hoisted. And then some people say like, they're moved to the top of their file. Wait, let me just show you. And that's just not like, I think that just creates confusion. Like here, because it's not that, they physically move to the top of the file.
[00:17:09]
That's not what's happening. It's just that they are created in memory with default values that we can use if it's a regular function declaration or a fart keyword, but they're not like physically moved to a different part of the file. Yeah, so same for modules, cuz that module electrical environment is created.
[00:17:28]
This is like that static part about like ES modules where JavaScript can already analyze and even tree shake. So like analyze that code for modules before that script even executes.
>> Lydia: Yeah.
>> Speaker 3: Does it mean if it's even were on line 13 or whatever, it's already hosted before the script is run?
[00:17:49]
>> Lydia: Yeah, so now if we, just gonna run this. So this is just logging some email in my phone, so I'm going to MJS. So here's the function some undefined on my phone, so this is before we even do that, but if I were to do the same with age username, you will see that it throws an error.
[00:18:08]
It cannot access age before initialization, but it still knows age, like it just knows it hasn't been initialized yet. So that's like the hoisting part. It's not that it doesn't have any reference in memory. No, it does, but uninitialized, yeah.
>> Speaker 2: How does strict mode affect this?
>> Lydia: Use strict console lock on some email.
[00:18:36]
Note and that's, that's fine. You just cannot like use, a strip mall you cannot just say like. newName is Lydia. Then it'll say we cannot, here, newName is not defined. But if I wasn't using use strict. Well, this is actually a module. [LAUGH] Modules are strict by default, so I'm just gonna create a new one here, test.js.
[00:18:58]
myName is Lydia, note test.js, that's fine. I'm gonna log it. Live coding is so scary. I always forget how I use my fingers. Okay, so here my name is Lydia. But then we do use strict and now it'll actually say like, complain reference error. My name is not defined.
[00:19:19]
That's like the biggest difference with use trick but again, also, if you're using modules, it'll do the same thing cuz modules are strict by default. So if I have like my name, Lydia, and I don't use use strict here, my name, it should throw the same error. It does, my name is not defined, without using use strict, but modules, and it's the extension MJS, Module js, it's strict by default, so just a, just an FYI, but it shouldn't affect hoisting, so, okay.
[00:19:50]
Very last question in that case, what is the output if you run this code and orange is our favorite fruit. Is it orange with the actual orange color in the terminal? Is it orange with that percentage see, no color for you syntax error or orange, but we're actually using the white text, which means that it's using the CSS value from the top of the scope.
[00:20:16]
>> Lydia: Right, the right answer is syntax error. Because you can see that. We have a block scope with switch. But afterward, we don't have any block and any brackets. Like case orange is just we have those parentheses, but it's not an actual block, so we're redeclaring a variable within the same block scope.
[00:20:35]
That's not possible, but what we can do is actually wrap this in brackets cuz now we're creating a new block scope for every case. So now we can like if one throw that syntax error, but luckily, I'm just going to show you VS code because it actually show you this little, red line.
[00:20:54]
If you can see it ever show concrete aura, crease the font size here. Because this is without the brackets and I'll actually tell you you cannot read Eclair the block scoped variable CSS, because we have to here, but what we can do is just wrap this in brackets and then it's all good.
[00:21:15]
So that's that scope again, same with here. Now it's fine because now we only have one CSS in this switch block, right? Cuz these are the brackets here. So this is one scope, make sense? All right, that was the last question for us, scope and closures.
Learn Straight from the Experts Who Shape the Modern Web
- In-depth Courses
- Industry Leading Experts
- Learning Paths
- Live Interactive Workshops