Quick Tip: Master Closures by Reimplementing Them from Scratch
This article was peer reviewed by Tim Severien and Michaela Lehr. Thanks to all of SitePoint’s peer reviewers for making SitePoint content the best it can be!
To say there are a lot of articles about closures would be an understatement. Most will explain the definition of a closure, which usually boils down to a simple sentence: A closure is a function that remembers the environment in which it was created. But how does it remember? And why can a closure use local variables long after those variables have gone out of scope? To lift the veil of magic surrounding closures, I’m going to pretend that JavaScript doesn’t have closures and can’t nest functions, and then we’re going to re-implement closures from scratch. In doing so, we’ll discover what closures really are and how they work under the hood.
[author_more]
For this exercise, I’ll also need to pretend that JavaScript has one feature it doesn’t really have. I’ll need to pretend that an ordinary object can be called as if it were a function. You may have already seen this feature in other languages. Python lets you define a __call__
method, and PHP has a special __invoke
method, and it’s these methods that are executed when an object is called as if it were a function. If we pretend that JavaScript has this feature too, here’s how that might look:
// An otherwise ordinary object with a "__call__" method
let o = {
n: 42,
__call__() {
return this.n;
}
};
// Call object as if it were a function
o(); // 42
Here we have an ordinary object that we’re pretending we can call as if it were a function, and when we do, the special __call__
method is executed, same as if we had written o.__call__()
.
With that, let’s now look at a simple closure example.
function f() {
// This variable is local to "f"
// Normally it would be destroyed when we leave "f"'s scope
let n = 42;
// An inner function that references "n"
function g() {
return n;
}
return g;
}
// Get the "g" function created by "f"
let g = f();
// The variable "n" should be destroyed by now, right?
// After all, "f" is done executing and we've left its scope
// So how can "g" still reference a freed variable?
g(); // 42
Here we have an outer function f
with a local variable, and an inner function g
that references f
‘s local variable. Then we return the inner function g
and execute it from outside f
‘s scope. But if f
is done executing, then how can g
still use variables that have gone out of scope?
Here’s the magic trick: A closure isn’t merely a function. It’s an object, with a constructor and private data, that we can call as if it were a function. If JavaScript didn’t have closures and we had to implement them ourselves, here’s how that would look.
class G {
// An instance of "G" will be constructed with a value "n",
// and it stores that value in its private data
constructor(n) {
this._n = n;
}
// When we call an instance of "G", it returns the value from its private data
__call__() {
return this._n;
}
}
function f() {
let n = 42;
// This is the closure
// Our inner function isn't really a function
// It's a callable object, and we pass "n" to its constructor
let g = new G(n);
return g;
}
// Get the "g" callable object created by "f"
let g = f();
// It's okay if the original variable "n" from "f"'s scope is destroyed now
// The callable object "g" is actually referencing its own private data
g(); // 42
Continue reading %Quick Tip: Master Closures by Reimplementing Them from Scratch%
LEAVE A REPLY
You must be logged in to post a comment.