Chapter 7: Closures in JavaScript – How They Work & Why They Matter

0:00 / 0:00
Report an issue

Welcome to Last Minute Lecture.

This free chapter overview is designed to help students review and understand key concepts.

These summaries supplement, not replace, the original textbook and may not be redistributed or resold.

For complete coverage, always consult the official text.

Welcome back to the Deep Dive.

We spent some time already, you know, digging into lexical scope, block scope, and that whole principal lease exposure poll idea.

Yeah, we're moving up a level.

We're tackling what many folks find, well, maybe the trickiest concept in JavaScript,

closure.

Yes,

closure.

It sounds intimidating, maybe.

It really can, but it's absolutely fundamental for building like efficient modern JS.

So we're diving straight into chapter seven of you don't know JS yet to really pull closure apart.

What it is, how we actually see it working, and why it's definitely not just some random side effect.

Our mission here is basically to make closure less scary.

It's honestly the key tool for structuring programs.

Well, you mentioned PRL, right?

Limiting access.

Closure kind of builds on that.

It lets us wrap up variables, keep them local, but still give specific inner functions access, often like way after the original scope seems to have disappeared.

Okay, so if lexical scope locks variables in a vault,

closure is like giving a special key to an inner function, a key that works even after the main vault guard, the outer function has like gone home.

That's a great way to put it.

And seriously, if you've ever written even a simple callback function, you know, one that uses a variable from outside its own little definition, you've used closure, you just might not have called it that.

Okay, so it's already happening under the hood a lot.

Oh, constantly.

It's the mechanism that makes huge things possible in JS modules,

object oriented ideas like private data, almost all the cool functional programming stuff.

So yeah, understanding how to see it and manage it.

It's pretty much essential if you're serious about JavaScript.

All right, let's get into it.

Where do we start?

Rules.

Rules first.

Rule number one, closure is about functions, only functions.

If there's no function instance involved, you're not dealing with closure, period.

Got it.

Functions only.

And you mentioned seeing it, observing it.

What does mean?

Right, the observational definition.

What can we actually point to in the code and say, aha, closure?

Well, two things need to happen.

First, you got to invoke a function.

Okay, call a function makes sense.

But second, and this is key, that function has to be invoked in a completely different part of the scope chain from where you originally defined it.

Different branch.

Yeah.

Okay, so if I define function A inside function B, and then call A right there inside B, that's not closure.

Nope.

That's just normal lexical scope doing its thing.

Predictable lookup.

You don't need closure for that, and you wouldn't really notice its specific effect.

We need that time travel aspect, the persistence.

Exactly.

We need to see that variable hanging around across time and sort of across different scope environments.

Let's use the lookup student example from the book.

It shows this really clearly.

Okay, walk us through it.

Sure.

So you define an outer function, lookup student, and it takes an ID.

Inside that function, you've got an array called students and the student ID variable from the parameter.

Now, the really important part,

lookup student returns an inner function.

Let's call it greet student, and it takes a greeting.

Okay, so the outer function makes and gives back the inner function, and we call the outer one twice, maybe lookup student six and lookup student 112,

and save those functions.

It returns into an array, maybe chosen students.

Perfect.

Now, here's the crucial bit.

Once those two calls to lookup student are finished, their jobs are done, right?

Their execution context is popped off the staff.

They're finished running.

So logically, the scope they created, the one holding the student's array and that specific student ID for each call, that scope should be cleaned up, gone, ready for garbage collection GC.

Those variables should just vanish.

Wait, yeah.

If the scope is gone, how can the inner function possibly work later?

If I call, say, chosen students later on, shouldn't it just crash?

Like reference error students is not defined.

Ah, but it doesn't crash, and that's because of closure.

The inner function greet student, it knows it refers to those outer variables, student and student ID.

That connection, that persistent link back to its birthplace variables, that's the closure.

So what closure actually does is it keeps those specific outer variables alive in memory.

They don't get garbage collected because the inner function instance, the one we saved in chosen students, still holds a reference, a link back to them.

So when I call chosen students zero later, it follows that link back, finds the student's array and the student ID, which was six in the first call, finding Sarah and successfully returns.

Hello, Sarah.

That success, the fact it didn't error out, that's your direct observation of closure.

It kept the state alive.

Wow.

Okay.

That is kind of like magic.

It holds onto its birth environment.

And you mentioned even tiny functions, like arrow functions.

Even a little arrow function, say, if you use one inside greet student, maybe pass to an array dot find method.

If that arrow function references students or student ID, then that specific arrow function instance holds the closure.

It proves any function, no matter how small, can create and hold onto the state.

That ability to keep states separate for each instance.

That sounds like the classic adder example, doesn't it?

Exactly.

The canonical closure example.

You define adder, which takes num one.

Inside, it returns another function, add two, which takes num two and returns num one plus num two.

Right.

So we call adder 10 and save the result as add 10 two.

Then we call adder 42 and save that result as add 40 town.

And why does this work separately?

Because each time you call adder, you create a brand new, completely separate instance of the inner add to a function.

And critically, each instance gets its own closure over its own specific num one variable.

Ah, so add 10 two has a closure where num one is forever 10.

And add 42 two has a closure where num one is 42.

They don't interfere.

Precisely.

When you call add 10 to 15, it uses its closed over 10 to give you 25.

Add 42 to eight uses its closed over 42 to give you 50.

Totally independent, it hammers home.

Closure is per instance of the function, not the definition itself.

Run the definition twice, get two closures.

Okay.

This instance thing is important and it leaves right into busting a big myth.

Right.

People sometimes think closure takes like a photograph of the variables value when the function is defined.

Yes.

Huge misconception.

It's absolutely not a static snapshot.

Closure is a live link.

It preserves access to the actual variable itself, which means you can read it.

Sure.

But you can also write to it.

You can change it.

You can change the closed over variable from the inside function.

You absolutely can.

Look at the make counter example.

The inner function closes over a count variable initialized to zero.

Okay.

Every single time you call the return function instance, say hits, it accesses that same closed over count, increments it and returns the new value.

So the first call gives one, the next gives two, then three.

Because it's modifying the one count variable preserved by its closure.

It's not resetting to zero each time.

Exactly.

It shows the live link in action.

The variable is maintained and mutated across calls.

And just to be super clear, the outer thing providing the scope doesn't even have to be a function.

You could just use a regular block scope, declare a variable with let inside it, define a function inside that block that uses the let variable and pass that function out.

It still works.

So the function holds the closure over the block scoped variable too.

Yep.

The rule is just inner function using a variable from some outer scope invoked somewhere else later.

Understanding this live link.

Okay.

Now the infamous loop problem makes more sense.

The one with VAR.

Yes, the classic trap.

You write a for loop like for i equals zero, i three, i plus plus.

And inside the loop, you create functions and save them maybe in an array called keeps.

Yeah.

You think you're saving three functions, one that should remember as zero, one for one, one for two.

Looks perfectly fine.

Then you call them later.

Keeps zero, keeps one, keeps two, and they all return.

Three.

It's so confusing the first time.

Why three?

Where does zero, one, and two go?

It goes straight back to how VAR works.

VAR has function scope or global scope, if not in a function.

So in that for loop, there's only one single i variable being created and updated.

Just one.

Oh.

Right.

It's not creating a new i each time through the loop.

Nope.

So all three functions you created inside the loop, they all close over that exact same single shared i variable.

By the time the loop finishes running, that shared i variable holds its final value, which is three.

And when you call the functions later, they all look up that same shared i and find three.

Ouch.

Ouch indeed.

So how do we fix it?

We need separate variables for separate iterations.

Right.

The old school manual way was to force a new scope inside the loop.

Like immediately inside the loop body, you'd write let ji.

Exactly.

Let creates a block scoped variable.

So each time the loop runs, it creates a brand new j, copies the current value of i into it, and the function created in that iteration closes over that specific j.

So you get closures over zero, one, and two, as expected.

Yep.

But thankfully we don't need that manual step anymore.

The modern clean fix is just use let in the loop header itself for let i war zero.

Ah, because let in a for loop header has that special behavior.

It does.

The language spec says that using let or const in a for loop header automatically creates a new variable binding for i for each iteration of the loop.

Problem solved elegantly and automatically.

That's much better.

Okay.

So now that we kind of get the mechanics, we probably see closure all over the place without thinking about it.

Absolutely.

Think about Ajax calls.

You might have a function lookup student record that takes an ID.

Inside, you define an on record callback function.

Okay.

Lookups your record kicks off the network request and then finishes.

Its scope is gone.

But much later, when the data comes back from the server, the on record function gets called.

How does it know which student ID the data belongs to?

Ah, closure.

It closed over the student ID variable from the lookup student record scope way back when it was defined.

Precisely.

Same with event handles.

You attach an on click function to a button.

Maybe that function needs to use a specific label that was passed into the setup function.

Hours later, someone clicks the button.

And the on click function still has access to that original label variable thanks to closure, even though the setup function finished long ago.

Okay.

Let's nail down the definition again.

The strict observable definition.

What are the three conditions?

One,

got to be a function involved.

Check.

Two,

that function has to reference at least one variable from an outer scope.

Makes sense.

And three, the observable proof.

It has to be invoked in a different scope branch from where those outer variables live.

That's what demonstrates the persistence.

Got it.

And just as useful is knowing what isn't closure,

observably speaking.

Right.

Calling a function in the same scope it was defined.

Just normal lexical scope.

Okay.

Accessing global variables.

Not closure.

Globals are always available anyway.

Right.

Also, if an inner function could see an outer variable but just never actually uses it, no closure is formed over that specific variable.

The engine can likely garbage collect it.

Okay.

So unused variables aren't kept alive just because they're in the scope.

Correct.

And finally, if you create an inner function that could form a closure, but you simply never call that inner function.

Well, you never observed the closure's effect.

So for all practical purposes, it didn't matter.

Okay.

So this link between closure and keeping variables alive, that sounds like it has big implications for memory management and garbage collection.

Huge implications.

Critically.

A variable kept alive by closure will stay in memory as long as there is any reference anywhere in your program to the function instance that closes over it.

Whoa.

So if I create a function, it closes over some big array and I attach it as an event handler to a button.

And you never remove that event handler.

Even if the button is hidden or removed from the page visually, that function instance still exists, referenced by the browser's event system.

And because the function exists, the closure exists, and that big array it closed over, it stays in memory forever.

Or until the page unloads.

That sounds like a memory leak waiting to happen.

It's a classic source of memory leaks in long -running single -page apps.

You have to be mindful of detaching handlers or otherwise breaking the reference chain when things are no longer needed, especially if they close over significant amounts of data.

Okay, that leads to a deep question.

When a closure happens, does the JavaScript engine keep alive only the specific variables the inner function uses?

Or does it keep the entire outer scope alive?

Ah, yeah.

The per variable versus per scope debate.

Conceptually and for efficiency, we like to think it's per variable.

Why keep stuff you don't need, right?

But the implementation reality, in many, maybe most JS engines, is often closer to per scope.

They tend to keep the whole scope object alive initially.

Then, as an optimization step, they might try to prune out the variables within that scope that aren't actually referenced by any closures.

I dread.

So it's optional.

We can't rely on it.

Exactly.

It's an optimization, not a guarantee.

And sometimes, the engine can't perform the optimization.

Like, remember evil.

Oh yeah, the scope cheat.

Right.

If the engine sees evil or was used inside a function, it often has to assume the worst that any variable in the surrounding scopes might be accessed dynamically.

So it disables the optimization and is forced to keep the entire scope chain alive, just in case.

Even variables, the inner function clearly doesn't touch.

Yikes.

So if I have a function that closes over a small variable, but that function lives inside another scope that holds a massive array.

And maybe you used evil somewhere.

That massive array might be kept in memory unnecessarily by the closure, even though your inner function never uses it.

Okay.

So what's the practical advice here?

Don't rely on the engine's optimization.

Pretty much.

If you have a variable inside an outer scope that holds a lot of data, and you know you won't need that data anymore after the outer function finishes,

except maybe for smaller pieces held by closure, it's safer to manually clear out the big variable yourself.

Like, set big array null right before the outer function returns.

Be explicit about releasing memory.

Exactly.

Don't let it hang around accidentally, just because it happens to share a scope with a variable that is needed by a closure.

Okay.

That makes sense.

Now, you mentioned earlier there's sort of an alternative way to think about closure, not the function moving and keeping a link back.

Yeah.

It's more of an implementational perspective, maybe easier for some folks to visualize.

Instead of thinking the function moves and carries its scope link, imagine the function instance itself kind of stays put.

It stays right there in its original lexical environment with its connection to its scope chain intact.

Okay, so the function instance doesn't move.

What moves, or what gets passed around your program, is just a reference to that function instance, like a pointer.

So in this model, closure isn't about the function carrying a backpack of variables.

It's more just.

The natural consequence of keeping a function instance alive via references.

As long as some part of your program holds a reference to that function instance, the instance itself and its entire original scope environment is prevented from being garbage collected.

So the magic is just standard object referencing, keeping things alive.

Pretty much.

It relies only on standard reference behavior.

The function instance stays alive because something points to it.

And because the instance is alive, its connection to its birth scope is alive.

Many find this model less, I don't know, magical and more grounded in how references work generally.

Interesting.

Two ways to picture the same outcome.

Whether it's a link back home or the home never getting demolished as long as the function is referenced.

The result is the same, right?

We get these benefits for organizing code.

Absolutely.

The practical benefits are huge.

First, efficiency, right?

The closure lets the function remember stuff.

Yeah, remember data it calculated before, configuration it was given, like the URL and data for an Ajax request and an event handler.

It doesn't have to figure that stuff out or read it from the DOM every single time it's invoked.

It just knows because it's closed over those values.

Saves work.

And the other big one is encapsulation, readability.

Definitely.

By hiding implementation details or state variables inside the closure scope, we limit their exposure.

This goes right back to Pohl principle of least exposure.

The function you get back might be really simple to use because all the complex setup is tucked away inside the closure, inaccessible from the outside.

Makes the interface cleaner.

Much cleaner.

And this encapsulation is really the foundation for more advanced patterns like modules or functional programming techniques like currying or partial application where you create functions that sort of preload some arguments via closure for later use.

So wrapping it up then.

Closure, yeah.

It's this mechanism in JavaScript where an inner function maintains a live link back to the variables of its outer and closing scope, even after that outer scope has finished executing.

Yep.

It allows functions to be stateful, to remember things across invocations.

It happens because the function instance itself keeps the necessary outer variables from being garbage collected.

We can observe it when a function is called in a different scope than where it was defined and successfully accesses those outer variables.

And we need to be careful about memory, especially with loops using var, use let, and by manually clearing large, unneeded variables within closed over scopes.

And whether you think of it as the function carrying a link back or the function's environment persisting as long as the function is referenced, the benefits are clear.

Efficiency through remembering and encapsulation through hiding details.

Okay.

So the final thought for you, the listener, to chew on.

Now that you really grasp how powerful closure is, how it makes variables persist potentially for a very long time, how does that fundamentally change how you think about designing functions,

especially functions that return other functions or behaviors?

How does it impact your approach to managing memory in applications that might run for hours, days, or even weeks?

Something really important to consider for robust applications.

Well, that's our deep dive into closure for today.

Thanks for joining us.

Yeah, thanks everyone.

See you next time.

ⓘ This audio and summary are simplified educational interpretations and are not a substitute for the original text.

Chapter SummaryWhat this audio overview covers
Closure represents a fundamental behavior in JavaScript that emerges naturally from lexical scoping and reinforces the Principle of Least Exposure by enabling functions to maintain persistent access to variables in their enclosing scopes long after those scopes have finished executing. A function exhibits observable closure behavior when it is invoked in a different branch of the scope chain than where it was originally defined, which prevents the referenced outer variables from being garbage collected. The critical distinction lies in closure creating a live link to the actual variable itself rather than capturing a static snapshot of its value at definition time, allowing for subsequent reassignment and reading. This understanding resolves common programming errors, such as unintended variable sharing in loops with var declarations, which can be addressed by using let to generate a fresh variable for each iteration, thereby establishing independent closures. Closure forms the foundation of modern JavaScript design patterns, particularly in functional programming paradigms including partial application and currying, and proves essential in asynchronous programming contexts such as event handlers and Ajax callbacks. While closure operates conceptually on a per-variable basis, preserving only the referenced variables in memory, many JavaScript engines may retain the entire scope environment, especially when features like eval() are employed, potentially creating memory retention concerns. Developers can mitigate this by explicitly nullifying large variables no longer needed within a scope. The chapter presents two complementary frameworks for understanding closure: an academic observational model in which the function carries a hidden reference to its defining scope, and an implementation-focused model in which the function instance remains fixed while closure ensures the associated scope environment persists as long as the function reference exists. Through closure, developers achieve efficient and maintainable code by encapsulating data within function instances, creating reusable and specialized tools that reduce complexity and improve code organization.

Using this chapter to study? Last Minute Lecture is free and student-run. If it helped, consider supporting the project.

Support LML ♥