Closures, Up Close
Closures are an important topic in JavaScript that cause developers a disproportionate amount of worry. If you’ve written more than a little JavaScript, you’ve probably created a closure without realizing. Does this look familiar?
// ...
ClickCounter.prototype.bindEventHandlers = function() {
var self = this;
var button = document.getElementById('btn');
button.addEventListener('click', function() {
self.buttonClickCount++;
});
};
That uses closures!
Put succinctly, closures are inner functions that use variables declared in their outer functions. This feature, however, is built upon lower-level concepts that often go overlooked. This is unfortunate, since we can use this foundation to help build better mental models that facilitate debugging and inform code design. In this post, I would like to explore these foundational concepts first, returning to closures after with appropriate context.
Function scope and hoisting
JavaScript uses function scoping, meaning that variables defined within a function are accessible anywhere in that function, including nested functions. Synonymously, functions have access to all variables defined within them and in any parent functions. This contrasts with block scope, where variables defined in blocks—like loops and try statements—are unavailable outside their blocks.
function firstFunctionScope() {
var frog = 'frog';
function inner() {
var fish = 'fish';
console.log(frog); // "frog";
console.log(fish); // "fish";
}
console.log(frog); // "frog";
console.log(fish); // ReferenceError: fish is not defined
}
function secondFunctionScope() {
var bird = 'bird';
console.log(bird); // "bird";
console.log(frog); // ReferenceError: frog is not defined
}
Furthermore, variables in JavaScript are hoisted, a term used to describe the apparent “hoisting” of their declarations to the top of the function. This is illustrated in the following example, where we refactor the first function into the second by shifting the declaration of each variable to the top of the function. Note that all subsequent assignments and expressions preserve their order. Ultimately, this means we can access a function’s variables at any point within scope, regardless of their position.
function declarations() {
var horse = 'horse';
for (var i = 0; i < 3; i++) {
var hare = i * 2;
}
var hat = function() {};
function hen() {};
}
// functionally equivalent
function hoistingDeclarations() {
function hen() {};
var horse, i, hare, hat;
horse = 'horse';
for (i = 0; i < 3; i++) {
hare = i * 2;
}
hat = function() {};
}
This can cause unexpected behavior at times, since a hoisted variable may be called before its value is assigned.
foo();
bar(); // Uncaught TypeError: bar is not a function
function foo(){} // hoisted
var bar = function(){}; // hoisted but not assigned
Hoisting and closures leverage a similar mechanism called the execution context.
Execution context and the scope chain
When a function is called in JavaScript, it creates an execution context containing the following information:
- The value of
this - A list of all its variable declarations (the variable object)
- The list of all its parents’ variable objects (the scope chain)
This information is collected before the body of the function is executed. Hoisting occurs during this stage when variables are aggregated in the variable object.
After the creation stage, the function continues normally where it uses and assigns its many variables. When it encounters a variable, it checks the variable object for that variable’s location in memory.
If, however, a function does not find the variable in its own
variable object, it travels down the scope chain, checking each of its
parent’s variable objects. It will continue searching its outer functions until
it reaches the global context (the code’s entry point, e.g. window), after
which it throws a ReferenceError.
Allow us to explore this concept by invoking the function from an earlier example.
function declarations() {
var horse = 'horse';
for (var i = 0; i < 3; i++) {
var hare = i * 2;
}
var hat = function() {};
function hen() {};
}
declarations('param', 10);
Doing so, we could represent its execution context as the following object,
var theoreticalExecutionContext = {
this: window,
variableObject: {
arguments: {
0: 'param'
1: 10,
length: 2
},
hen: {}, // reference to function hen
hare: 4,
hat: {}, // reference to anonymous function
horse: 'horse',
i: 3
},
scopeChain: [] // this obj's variableObject, window's variableObject
};
Closures
With a rough idea of how execution contexts and the scope chain work, we are able to see the truth behind closures. Now we understand closures as a concept in addition to just a fancy word.
Event handler callbacks
Let us revisit our first example, rewritten slightly, to clarify what is going on under the hood when binding our event handler.
ClickCounter.prototype.bindEventHandlers = function() {
var self, button;
function handler() {
self.buttonClickCount++;
}
self = this;
button = document.getElementById('btn');
button.addEventListener('click', handler);
};
When called, our function first creates its execution context. Here, the value
of this is the object to which the function belongs—an instance of
ClickCounter. Its variable object contains keys to two variables, self and
button, and an inner function, handler.
The function body then executes, assigning to self our class instance and
assigning to button an object representing a button element on document (the web page
loading the script). Finally, it calls button.addEventListener,
which sets up an event listener that will call handler when the button is
clicked.
Later, when the button is clicked, it calls handler. However, since the
button is calling the function (not of the instance of ClickCounter), the
value of this in handler’s execution context is the button. If we would have
used this in the callback instead of self, we would have incorrectly
incremented buttonClickCount on the button.
As written, handler looks in its own variable object for a reference to self.
Failing to locate it there, it traverses its scope chain and finds it
in its parent’s variable object. Doing so correctly increments the property on
our class instead of the button.
Private variables
Another common use of closures is the creation of “private” variables. The example below, an example of the revealing module pattern, returns an object that only contains pointers to the variables and functions the author wishes to expose. Thanks to closures, these exposed functions may still access variables and functions persistent long after their defining function returned.
function SecretiveClass() {
var privateVariable = 10;
var publicVariable = 20;
function privateMethod(public, private) {
return public * private;
}
function publicMethod(public) {
if (public) {
publicVariable = public;
}
return privateMethod(publicVariable, privateVariable);
}
return {
publicVariable: publicVariable,
publicMethod: publicMethod
};
}
var instance = SecretiveClass();
typeof instance; // "object"
typeof instance.privateVariable; // "undefined"
typeof instance.privateMethod; // "undefined"
typeof instance.publicVariable; // "number"
typeof instance.publicMethod; // "function"
Summary
To summarize, every function stores a variable object containing a reference to all the functions and variables defined within it. It also sets up a scope chain, which contains the variable object of its parent and any other ancestors it may have. When accessing a variable, it checks its own variable object for a variable with that name. If it doesn’t find it there, it traverses its ancestors’ variable objects via its scope chain. These variable objects persist as long as there is a reference to them.
The examples above are only two of the many applications of closures in JavaScript. Hopefully with a basic understanding of scope, execution contexts, and the scope chain, you will be able to identify, understand, and debug closures more easily in your own code and in any code you may come across.
Bibliography
For additional exploration into these topics, check out the following links, which provided the basis of this post:
- What is the Execution Context & Stack in JavaScript? by David Shariff
- Identifier Resolution and Closures in the JavaScript Scope Chain by David Shariff
- Understanding Scope and Context in JavaScript by Ryan Morr
- Javascript Closures by Richard Cornford