You Don't Know JS: Scope & Closures - podcast episode cover

You Don't Know JS: Scope & Closures

Apr 16, 202522 min
--:--
--:--
Download Metacast podcast app
Listen to this episode in Metacast mobile app
Don't just listen to podcasts. Learn from them with transcripts, summaries, and chapters for every episode. Skim, search, and bookmark insights. Learn more

Episode description

This Book is an excerpt from Kyle Simpson's book, "You Don't Know JS: Scope & Closures" which comprehensively explains JavaScript scope and closures. The excerpt includes a foreword and preface discussing the challenges and complexities of mastering JavaScript, emphasizing the importance of deep understanding rather than superficial knowledge. The main body provides a detailed explanation of scope, differentiating between lexical and dynamic scope, and illustrating how the compiler handles variable declarations and assignments through a process called hoisting. Finally, the excerpt thoroughly examines closures, explaining how functions maintain access to their lexical environment even when executed outside of it, and how this concept underlies various programming patterns like modules.

You can listen and download our episodes for free on more than 10 different platforms:
https://linktr.ee/cyber_security_summary

Get the Book now from Amazon:
https://www.amazon.com/You-Dont-Know-JS-Closures/dp/1449335586?&linkCode=ll1&tag=cvthunderx-20&linkId=57f14a039184e84987da28bbb39358c3&language=en_US&ref_=as_li_ss_tl

Discover our free courses in tech and cybersecurity, Start learning today:
https://linktr.ee/cybercode_academy

Transcript

Speaker 1

Welcome to the deep dive. Today. We're going deep on something that seems, I don't know, deceptively simple in JavaScript variables we use them all the time obviously, but have you ever stopped to think about like where they actually live in the code and how the JavaScript engine actually finds them. You know, That's what scope is all about, and believe me, it gets way more interesting than you

might think it does. Our guide for this deep dive is Kyle Simpson's book You Don't Know JS Scope Enclosures. He really digs deep into like the guts of how JavaScript works, and we're going to unpack some of the key insights from his work. Hopefully, you know, make it clear. By the end of this you'll have a rock solid understanding of scope and it's close relative closures, which will make you, I hope, a much more confident JavaScript developer.

Right off the bat, Kyle reveals something that kind of blew my mind. JavaScript is actually a compiled language. I always thought it was like interpreted.

Speaker 2

I know, it's a common misconception. A lot of people think that, well, JavaScript isn't compiled ahead of time like some languages, you know, C plus plus or something. The engine actually does compile your code right before execution. Okay, it's incredibly fast, like microseconds, But it does involve like breaking down the code into tokens, parsing the structure, and then actually generating the instructions the computer's going to run.

Speaker 1

Huh. So it's not just reading the code like line by line as it goes. No, not at all interesting.

Speaker 2

Yeah, this compilation step is really crucial for how JavaScript handles scope. So think of scope as a set of rules that determine where variables live and how the code can access them.

Speaker 1

Okay, that makes sense. Kyle uses this idea of LHS and RHS lookups to explain how this works. Can you break that down a little bit? Sure.

Speaker 2

Imagine the JavaScript engines like having a conversation with a friend called scope. Okay, So when the engine needs to assign a value to a variable, like you know, in a statement like two, it basically asks Scope, Hey, I need to put this value two tons somewhere. Do you have a container labeled A Okay, So that's an LHS lookup. It's looking for the target of the assignment.

Speaker 1

So it's like finding the right box to put something in exactly.

Speaker 2

Now, if the code needs to actually read the value of a variable. Like in console dot log the engine asks, so, hey, can you tell me what's inside the container labeled A. That's an RHS lookup. It's retrieving the value.

Speaker 1

Got it. So LHS is for assigning, RHS is for retrieving. But how does this relate to nested scopes, like you know when you have functions within functions.

Speaker 2

Yeah, so think of nested scopes kind of like floors in a building. Okay, each function creates a new floor. If the engine can't find a variable on the current floor, it takes the elevator up to the next level, searching each enclosing scope until it reaches the top floor, which is the global scope.

Speaker 1

Global scope, and if it can't find the variable even after going all the way up, then.

Speaker 2

You get an error and you get a reference error for RHS lookups, meaning the variable doesn't exist, and they type error for LHS lookups if you're trying to assign to something that can't be assigned to.

Speaker 1

Got it?

Speaker 2

Yeah?

Speaker 1

Okay, so scope is all about where the code can see the variables. But then what about lexical scope. That sounds a bit more I don't know, complex.

Speaker 2

Yeah, lexical scope just simply means that the scope of a variable is determined by where it is written in the code, not by where it's executed. It's like those Russian nesting dolls. Yeah, each scope is contained within another.

Speaker 1

Okay, So if I have a function inside another function, the inner function can see the variables from the outer function exactly.

Speaker 2

Okay, the interfunction scope is nested within the outer function scope, and if there's a variable with the same game in both scopes, the inner function will prioritize its own local variable. Effectively, it's kind of like shadowing the one from the outer scope.

Speaker 1

So lexical scope is all about the structure of the code itself. Are there ways to break these rules?

Speaker 2

Yeah, there are a couple ways to cheat lexical scope, but they're generally frowned upon because it makes your code harder to understand and potentially like less performant. One is using evil, which lets you execute arbitrary code dynamically, potentially changing the scope at runtime, so.

Speaker 1

That throws a wrench into the whole like compile time scope analysis exactly.

Speaker 2

The other way is to mess with lexical scope is using the with statement. It's less common, but it creates a new scope from an object, which can lead to confusion and potentially unintended consequences.

Speaker 1

So probably best to avoid those.

Speaker 2

If we can, yes stick to the clear rules of lexico scope, it's going to keep your code more predictable and help the engine optimize it better.

Speaker 1

This is making a lot more sense. But so far we've only talked about like functions creating scopes. What about other parts of the code, like loops, conditional statements, those sorts of things. Do they have their own scopes?

Speaker 2

That's a great question. So for a long time, JavaScript only had what's called function scope, meaning functions were the main creators of scope.

Speaker 3

But with six we now have block scope as well block scope, so now blocks of code can have their own little, I don't know, isolated spaces for variables exactly.

Speaker 2

It's all about this idea of keeping variables like as contained as possible, following the principle of least privilege.

Speaker 1

That sounds like a good security practice.

Speaker 2

It is. It helps prevent accidental reuse of variables and potential conflicts. Leads to cleaner, more maintainable code.

Speaker 1

So how do you create a block scope? Is it just curly braces?

Speaker 2

So not all curly braces create a new scope. Traditionally, four loops didn't actually have their own block scope, which led to some interesting behavior. But with ES six we have the let and constant keywords. They allow us to declare variables that are specifically scope to the block they're defined in.

Speaker 1

Okay, so if I use let inside of a four loop, that variable is truly contained within that loop exactly.

Speaker 2

It's a much cleaner way to manage variables and prevent them from like accidentally leaking into other parts to the code. And const is very similar, except it's for declaring constants values that shouldn't be reassigned.

Speaker 1

I gotcha. This is all making a lot more sense now. But there's one more thing that's always kind of tripped me up when it comes to scope hoisting. It's like variables can like magically move around in the code.

Speaker 2

Yeah. Hoisting is another one of those things that kind of highlights the importance of that compilation step we talked about ear Yeah, so during compilation, the JavaScript engine actually processes all the variable and function declarations first before any other code is executed, so.

Speaker 1

It's like they're all moved up to the top of their scope.

Speaker 2

Yeah, that's a good way to visualize it mm, and it can lead to some unexpected behavior if you're not careful. For example, you can actually use a variable before it appears to be declared in the code because the declaration has already been hoisted during compilation.

Speaker 1

So I can use a variable before it's declared.

Speaker 2

You can use it in the sense that the engine knows it exists, but it won't have a value yet.

Speaker 1

Okay.

Speaker 2

This is where the difference between like declaration and assignment comes in.

Speaker 1

Right, So declaring a variable is just telling the engine it exists. Assigning of value is when you actually put something in that container. So hoisting only applies to declarations, not assignments exactly.

Speaker 2

The assignment still happens where you write it in the code, but the declaration is processed beforehand, which can lead to like some head scratching moments if you're not aware of it.

Speaker 1

That gotcha. Okay, I think I'm starting to wrap my head around hoisting a little bit. Any other I don't know quirks I should be aware of.

Speaker 2

One kind of interesting thing is that function declarations are hoisted before variable declarations. So if you have a function and a variable with the same name, the function declaration will actually take precedence. But in general, you know, it's good practice to just avoid these kinds of situations and write like clear and predictable code.

Speaker 1

It sounds like solid advice.

Speaker 2

Yeah.

Speaker 1

Okay, So we've covered scope, lexical scope, breaking the rules, block scope, and hoisting. We've gone pretty deep into how JavaScript handles variables. But there's one more big concept that always seems like shrouded in mystery. Closures.

Speaker 2

Closures.

Speaker 1

Yeah, it's often seen as this advanced, almost magical concept in JavaScript, but it's actually simpler than it sounds. It's something that just happens naturally in your code all the time.

Speaker 2

Really, I've always been like kind of intimidated by closures. Yeah, what exactly is a closure? And why should I even care about them?

Speaker 1

So a closure is simply a function's ability to remember and access its lexical scope even when it's executed outside of that original scope. Okay, it's a natural consequence of how scope works in JavaScript.

Speaker 2

Can you give me an example of that. I'm still a little hazy on what that actually, you know what that looks like?

Speaker 1

Sure? Imagine you have a function called outer function, and it, you know, declares a variable called message inside of our function, you define another function, inner function right now, because of lexical scope, inner function can access the message variable from out.

Speaker 2

Function, right, because you know, the inner function can see.

Speaker 1

Those variables exactly. But here's the interesting part. If outer function returns inner function like gives it back to the outside world.

Speaker 2

Right, and we later execute inner function, it can still access that message variable even though outer function is finished running.

Speaker 1

Wait, so interfunction is holding onto that scope even after outer function is done exactly.

Speaker 2

That's closure in action. Interfunction is closed over the scope of outer function, preserving its access to the variables in that scope.

Speaker 3

Wow.

Speaker 2

And this is incredibly powerful because it allows functions to maintain their own private state even as they're passed around and executed in different parts of the code.

Speaker 1

Okay, I'm starting to see why closures are a big deal. But where do we actually encounter them in real world code? Like, is this just a theoretical concept or are closures actually doing useful stuff behind the scenes.

Speaker 2

Closures are incredibly common. They're at work whenever you use callbacks, timers, event handlers, pretty much anything synchronous in JavaScript.

Speaker 1

Oh okay.

Speaker 2

For example, when you pass a function to set time out, that function is going to be executed later, long after the code that define its finished running, but thanks to closure, it still has access to the variables it needs from its original scope.

Speaker 1

So closures are like a time machine for scope. They allow functions to like travel through time and you know, carry their context with them.

Speaker 2

That's a great way to put it. Wow. Yeah, it's one of the things that makes JavaScript so flexible and powerful.

Speaker 1

Okay, I'm starting to like see the light here. Yeah, but there's one particular scenario involving closures that always kind of baffled me. Closures and loops. Yeah, I've run into some like unexpected behavior when combining those two.

Speaker 2

Closures and loops can definitely be tricky. Yeah, it's a classic case where our intuition might not match like how JavaScript actually works. Let's delve into that next time, explore why it can be so confusing, and more importantly, how to handle it effectively.

Speaker 1

Yeah, let's do it. Welcome back to the deep dive. Last time we kind of dove into that fascinating world of JavaScript scope enclosures, we even you know, started to unravel some of those mysteries surrounding them. Today we're going to tackle I think one of the trickiest scenarios involving closures, closures within loops. I've run into like some head scratching moments with this before. Yeah, and I'm sure you know a lot of other JavaScript developers have too.

Speaker 2

Oh yeah, absolutely, it can definitely be a bit of a minefield. You to be careful with those. Yeah, it's one of those places where, like the asynchronous nature of JavaScript can really throw you for a loop. It's it often trips up even experienced developers.

Speaker 1

Okay, so let's kind of set the stage here. What's the typical scenario where closures and loops like collide?

Speaker 2

So one really common one is when you're trying to set up a series of delayed actions using set time out within a loop, Okay, where each timeout is supposed to like reference the loop counter.

Speaker 1

Right.

Speaker 2

It seems really straightforward at first, but the results can be kind of surprising.

Speaker 1

I think I've been there. You Like, you expect each time out to capture the current value of the loop counter, but things don't quite work out the way you think they're going to. Can you can you give us an example like walk us through that?

Speaker 2

Sure? Sure, So let's say we have a loop that iterates you know from one to five, okay, and within each iteration we set a time out that logs the current value of the loop counter after you know, one second delay. The code might look, you know, something like this. So we've got for barkyei one I expense five I plus plus set time out function I one thousand.

Speaker 1

Okay, similar enough. I have a feeling it's not going to do what we expect though.

Speaker 2

Yeah, you're right to be suspicious.

Speaker 1

Yea.

Speaker 2

If you actually run this code, you'll see it logs the number six five times, all after the loop's completed.

Speaker 1

Wait a minute, six, But the loop only goes to five, right, and why is it logging it like five times? What's going on there?

Speaker 2

It all kind of boils down to that combination of scope closures and the asynchronous nature of set timeout. When we create those timeout functions within the loop, they each form a closure capturing a reference to the variable eye from the surrounding scope.

Speaker 1

Okay, so they have a like connection back to that to that variable.

Speaker 2

Yeah, exactly. However, those timeout functions don't execute right.

Speaker 1

Away right they're delayed.

Speaker 2

They're scheduled to run after a delay, and by the time they actually get around to executing the loop is already finished, and the value of eye has reached six.

Speaker 1

So they're all looking at the same eye, which has been incremented beyond the loop, like the range of the loop exactly. But why does it log six five times? Why wouldn't it just log it once at the very end when the loop's done.

Speaker 2

So we see six five times because we created five timeout functions, each one with its own closure over the same eye variable.

Speaker 1

Got it.

Speaker 2

They all essentially have this shared memory of that variable, and they all see its final value when they finally do run.

Speaker 1

Okay, it's like they have a delayed reaction, but by the time they react, the world has changed and the value of I is no longer what they thought it was going to be.

Speaker 2

That's a great analogy. So the question is, how do we fix this? How do we make each timeout capture the right value of I at the time it was defined.

Speaker 1

H that's a good question. I mean we need, I guess a way to like isolate each timeout scrip so that it has its own kind of private copy of the loop counter. And I remember you mentioned last time aie s those like immediately invoked function expressions.

Speaker 2

Yeah, you're on the right track iifes are a classic solution to this problem.

Speaker 1

Okay.

Speaker 2

By wrapping that code inside the loop in an IEF, we can create a new scope for each iteration. So it looks something like this we've got for our I one I will five I plus plus set timeout functionconsole dot log I.

Speaker 1

Okay, so we're creating like a self executing function with in each iteration of the loop and passing in the current value of I into it. And I see we're also using like a different variable name J inside the IQ. What's that for?

Speaker 2

Yeah, so that J variable inside the IQ. It creates a new, like distinct variable within the ip zone scope. Okay, And that ensures that each timeout function has its own like private copy of the loop counter, got it, effectively isolating it from the outer loop's eye.

Speaker 1

Right. Okay, so we're kind of creating like these little isolated scope bubbles exactly.

Speaker 2

That prevents those those timing conflicts. And you might remember from last time E six and introduced an even more elegant solution for this using the let keyword. Oh, let creates block scoped variables, right, So when you use let in the loop heeader, it effectively creates a new binding for that loop counter on.

Speaker 1

Each iteration, so we can actually just like simplify that code even further. We don't even need that if exactly.

Speaker 3

Wow.

Speaker 2

So the E S six version using let would look like this for let I one, I five, I plus plus. Yeah, set time out, function building much clean.

Speaker 1

Yeah. Wow, the let keyword just takes care of all that for us. Yeah, this block soaping stuff is pretty powerful.

Speaker 2

It is.

Speaker 1

So we've seen how closures within loops can kind of lead to this unexpected behavior, but we've also learned and how to kind of tame them using II fees or you know, that let keyword. Now, I can't help but wonder if these same kind of scenarios like pop up in other areas of JavaScript, especially when we're dealing with like, you know, asynchronous operations like ajax requests, those sorts of things.

Speaker 2

Yeah, that's a great observation. Closures in asynchronous operations like go hand in hand, right, and those timing intricacies that we've been talking about, they can lead to similar similar challenges. Right, So let's let's explore that a little bit further and see how these same principles and solutions that we've talked about applying those scenarios.

Speaker 1

Welcome back to the deep dive. We've we've really explored this like intricate dance between scope enclosures in JavaScript and even tackled that tricky terrain of closures within loops. Now it's time to bring it all together. See how these closures actually underpin one of the most powerful patterns in JavaScript, the module pattern.

Speaker 2

Module pattern, Right, it's all about creating like organized, reusable units of code that have their own kind of internal workings and a clear interface for interacting with the outside world.

Speaker 1

Yeah. Yeah, it's like building those self contained components, right that we can just plug and play into different parts of our application. But how do closures actually enable us to create these modules?

Speaker 2

So, closures are really the key to creating private data and functionality within a module. Okay, Remember how they allow functions to hold onto their lexical scope even when they're executed outside of that scope. Right, we can leverage that to create like a private compartment within our modules.

Speaker 1

Okay, I'm starting to see the connection here. Can you give us like a concrete example walk us through that.

Speaker 2

Sure, let's imagine we're building a simple countermodule. Okay, keep the actual counter value hidden you know, from the outside world, allow external code to only interact with it through specific methods like increment, decrement and get value.

Speaker 1

Right right, So we're like creating a controlled environment where the counter is kind of protected from you know, accidental manipulation or something exactly.

Speaker 2

So here's how we can achieve that using the module pattern. So we've got vur countermodule function for account increment dot function for account in function return count, but directly value dot function get value dot function.

Speaker 1

Okay, so we're defining a function called counter module and then immediately invoking it. What's the purpose of that?

Speaker 2

Yeah, so that's uh, that's the essence of an immediately invoked function expression or ioev. By wrapping our module code in an IF, we create this new scope that's isolated from the global scope, got it.

Speaker 1

So we're creating that like private compartment you were talking about exactly.

Speaker 2

Now, Notice how we have a variable count inside of the IF. That variable is only accessible within the scope of that IA, making it private to our module.

Speaker 1

And I see we're returning an object from the IF. Is that our public api exactly?

Speaker 2

That returned object contains the methods that we actually want to expose to the outside world increment, decrement, and get value.

Speaker 1

So external code can call those methods, but it can't directly mess with the count variable itself exactly.

Speaker 2

And here's where closures come in. Okay, those methods, because they were defined within the IF, they form closures over the if scope, so.

Speaker 1

They have a memory of that internal scope even after the IFE has finished executing.

Speaker 2

Precisely, that's how they can still interact with the private count variable even though it's not directly accessible from the outside.

Speaker 1

This is so clever. So by leverage enclosures, we've created this like secure encapsulated module with its own internal state and controlled access.

Speaker 2

It's incredibly powerful. It's the foundation of many popular JavaScript libraries and frameworks.

Speaker 1

Now, I remember you mentioned that ES six introduced native support for modules. Does that make it this kind of traditional module pattern obsolete.

Speaker 2

ES six modules definitely provide a more modern and streamlined way to work with modules, but that traditional pattern is still relevant. I think it's valuable to understand the underlying principles, and it can still be useful in certain situations, especially if you're working with older codebases.

Speaker 1

Right, It's like understanding the roots of a tree even as it's growing new branches exactly. So what are like, what are the main differences between ES six modules and this kind of more traditional pattern.

Speaker 2

So ES six modules use explicit important export statements to actually define the module's interface and how it interacts with other modules. Got it. So it's a more declarative approach. It's like build into the language itself.

Speaker 1

Right, So instead of relying on IF's andclosures to create that encapsulation, now we have like dedicated syntax.

Speaker 2

For it exactly. And it comes with some advantages. You know. E six modules are statically analyzable, okay, meaning the module structure is determined at compile time, which can lead to performance benefits. They're also more aligned with modern javascripts development practices. It just makes the code easier to read, reason about, and maintain.

Speaker 1

Cool. So it's definitely the way to go for new projects. But it's also great to have that understanding of the traditional module pattern in our back pocket.

Speaker 2

Yeah. Absolutely, I think it helps you appreciate the evolution of JavaScript and the clever techniques people have used to overcome challenges and build you know, more robust applications.

Speaker 1

Well, we've covered a lot of ground in this deep dive into JavaScript scope and closures, from understanding like the basic rules of variable access to seeing how closures can actually empower us to create this modular, maintainable code. It's it's been a really enlightening journey, I think.

Speaker 2

Yeah, I'm glad you found it valuable. Remember these concepts, they might seem a little bit abstract at first, but they're like fundamental to mastering JavaScript and writing you know, truly elegant and powerful code. Keep exploring, keep experimenting, and keep pushing the boundaries of your JavaScript knowledge.

Speaker 1

Awesome. Well, thanks for joining us on the deep dive, and until next time, happy coding.

Transcript source: Provided by creator in RSS feed: download file
For the best experience, listen in Metacast app for iOS or Android