The Closure Lifecycle and Garbage Collection (GC)

Since closure is inherently tied to a function instance, its closure over a variable lasts as long as there is still a reference to that function.

If ten functions all close over the same variable, and over time nine of these function references are discarded, the lone remaining function reference still preserves that variable. Once that final function reference is discarded, the last closure over that variable is gone, and the variable itself is GC’d.

This has an important impact on building efficient and performant programs. Closure can unexpectedly prevent the GC of a variable that you’re otherwise done with, which leads to run-away memory usage over time. That’s why it’s important to discard function references (and thus their closures) when they’re not needed anymore.

Consider:

  1. function manageBtnClickEvents(btn) {
  2. var clickHandlers = [];
  3. return function listener(cb){
  4. if (cb) {
  5. let clickHandler =
  6. function onClick(evt){
  7. console.log("clicked!");
  8. cb(evt);
  9. };
  10. clickHandlers.push(clickHandler);
  11. btn.addEventListener(
  12. "click",
  13. clickHandler
  14. );
  15. }
  16. else {
  17. // passing no callback unsubscribes
  18. // all click handlers
  19. for (let handler of clickHandlers) {
  20. btn.removeEventListener(
  21. "click",
  22. handler
  23. );
  24. }
  25. clickHandlers = [];
  26. }
  27. };
  28. }
  29. // var mySubmitBtn = ..
  30. var onSubmit = manageBtnClickEvents(mySubmitBtn);
  31. onSubmit(function checkout(evt){
  32. // handle checkout
  33. });
  34. onSubmit(function trackAction(evt){
  35. // log action to analytics
  36. });
  37. // later, unsubscribe all handlers:
  38. onSubmit();

In this program, the inner onClick(..) function holds a closure over the passed in cb (the provided event callback). That means the checkout() and trackAction() function expression references are held via closure (and cannot be GC’d) for as long as these event handlers are subscribed.

When we call onSubmit() with no input on the last line, all event handlers are unsubscribed, and the clickHandlers array is emptied. Once all click handler function references are discarded, the closures of cb references to checkout() and trackAction() are discarded.

When considering the overall health and efficiency of the program, unsubscribing an event handler when it’s no longer needed can be even more important than the initial subscription!

Per Variable or Per Scope?

Another question we need to tackle: should we think of closure as applied only to the referenced outer variable(s), or does closure preserve the entire scope chain with all its variables?

In other words, in the previous event subscription snippet, is the inner onClick(..) function closed over only cb, or is it also closed over clickHandler, clickHandlers, and btn?

Conceptually, closure is per variable rather than per scope. Ajax callbacks, event handlers, and all other forms of function closures are typically assumed to close over only what they explicitly reference.

But the reality is more complicated than that.

Another program to consider:

  1. function manageStudentGrades(studentRecords) {
  2. var grades = studentRecords.map(getGrade);
  3. return addGrade;
  4. // ************************
  5. function getGrade(record){
  6. return record.grade;
  7. }
  8. function sortAndTrimGradesList() {
  9. // sort by grades, descending
  10. grades.sort(function desc(g1,g2){
  11. return g2 - g1;
  12. });
  13. // only keep the top 10 grades
  14. grades = grades.slice(0,10);
  15. }
  16. function addGrade(newGrade) {
  17. grades.push(newGrade);
  18. sortAndTrimGradesList();
  19. return grades;
  20. }
  21. }
  22. var addNextGrade = manageStudentGrades([
  23. { id: 14, name: "Kyle", grade: 86 },
  24. { id: 73, name: "Suzy", grade: 87 },
  25. { id: 112, name: "Frank", grade: 75 },
  26. // ..many more records..
  27. { id: 6, name: "Sarah", grade: 91 }
  28. ]);
  29. // later
  30. addNextGrade(81);
  31. addNextGrade(68);
  32. // [ .., .., ... ]

The outer function manageStudentGrades(..) takes a list of student records, and returns an addGrade(..) function reference, which we externally label addNextGrade(..). Each time we call addNextGrade(..) with a new grade, we get back a current list of the top 10 grades, sorted numerically descending (see sortAndTrimGradesList()).

From the end of the original manageStudentGrades(..) call, and between the multiple addNextGrade(..) calls, the grades variable is preserved inside addGrade(..) via closure; that’s how the running list of top grades is maintained. Remember, it’s a closure over the variable grades itself, not the array it holds.

That’s not the only closure involved, however. Can you spot other variables being closed over?

Did you spot that addGrade(..) references sortAndTrimGradesList? That means it’s also closed over that identifier, which happens to hold a reference to the sortAndTrimGradesList() function. That second inner function has to stay around so that addGrade(..) can keep calling it, which also means any variables it closes over stick around—though, in this case, nothing extra is closed over there.

What else is closed over?

Consider the getGrade variable (and its function); is it closed over? It’s referenced in the outer scope of manageStudentGrades(..) in the .map(getGrade) call. But it’s not referenced in addGrade(..) or sortAndTrimGradesList().

What about the (potentially) large list of student records we pass in as studentRecords? Is that variable closed over? If it is, the array of student records is never getting GC’d, which leads to this program holding onto a larger amount of memory than we might assume. But if we look closely again, none of the inner functions reference studentRecords.

According to the per variable definition of closure, since getGrade and studentRecords are not referenced by the inner functions, they’re not closed over. They should be freely available for GC right after the manageStudentGrades(..) call completes.

Indeed, try debugging this code in a recent JS engine, like v8 in Chrome, placing a breakpoint inside the addGrade(..) function. You may notice that the inspector does not list the studentRecords variable. That’s proof, debugging-wise anyway, that the engine does not maintain studentRecords via closure. Phew!

But how reliable is this observation as proof? Consider this (rather contrived!) program:

  1. function storeStudentInfo(id,name,grade) {
  2. return function getInfo(whichValue){
  3. // warning:
  4. // using `eval(..)` is a bad idea!
  5. var val = eval(whichValue);
  6. return val;
  7. };
  8. }
  9. var info = storeStudentInfo(73,"Suzy",87);
  10. info("name");
  11. // Suzy
  12. info("grade");
  13. // 87

Notice that the inner function getInfo(..) is not explicitly closed over any of id, name, or grade variables. And yet, calls to info(..) seem to still be able to access the variables, albeit through use of the eval(..) lexical scope cheat (see Chapter 1).

So all the variables were definitely preserved via closure, despite not being explicitly referenced by the inner function. So does that disprove the per variable assertion in favor of per scope? Depends.

Many modern JS engines do apply an optimization that removes any variables from a closure scope that aren’t explicitly referenced. However, as we see with eval(..), there are situations where such an optimization cannot be applied, and the closure scope continues to contain all its original variables. In other words, closure must be per scope, implementation wise, and then an optional optimization trims down the scope to only what was closed over (a similar outcome as per variable closure).

Even as recent as a few years ago, many JS engines did not apply this optimization; it’s possible your websites may still run in such browsers, especially on older or lower-end devices. That means it’s possible that long-lived closures such as event handlers may be holding onto memory much longer than we would have assumed.

And the fact that it’s an optional optimization in the first place, rather than a requirement of the specification, means that we shouldn’t just casually over-assume its applicability.

In cases where a variable holds a large value (like an object or array) and that variable is present in a closure scope, if you don’t need that value anymore and don’t want that memory held, it’s safer (memory usage) to manually discard the value rather than relying on closure optimization/GC.

Let’s apply a fix to the earlier manageStudentGrades(..) example to ensure the potentially large array held in studentRecords is not caught up in a closure scope unnecessarily:

  1. function manageStudentGrades(studentRecords) {
  2. var grades = studentRecords.map(getGrade);
  3. // unset `studentRecords` to prevent unwanted
  4. // memory retention in the closure
  5. studentRecords = null;
  6. return addGrade;
  7. // ..
  8. }

We’re not removing studentRecords from the closure scope; that we cannot control. We’re ensuring that even if studentRecords remains in the closure scope, that variable is no longer referencing the potentially large array of data; the array can be GC’d.

Again, in many cases JS might automatically optimize the program to the same effect. But it’s still a good habit to be careful and explicitly make sure we don’t keep any significant amount of device memory tied up any longer than necessary.

As a matter of fact, we also technically don’t need the function getGrade() anymore after the .map(getGrade) call completes. If profiling our application showed this was a critical area of excess memory use, we could possibly eek out a tiny bit more memory by freeing up that reference so its value isn’t tied up either. That’s likely unnecessary in this toy example, but this is a general technique to keep in mind if you’re optimizing the memory footprint of your application.

The takeaway: it’s important to know where closures appear in our programs, and what variables are included. We should manage these closures carefully so we’re only holding onto what’s minimally needed and not wasting memory.