Scope and Closures¶
This reading introduces you to the concepts of scope and extent in programming languages, and eventually to closures. We are discussing these because:
- scope and closures are very important in computer science and in coding
- closures are relevant and important in web programming
- closures are one way to understand
bind
, which we'll get to later in the course.
Closures¶
A closure is a special kind of function. Consequently, you'll learn more about them in CS 251 (Programming Languages), but they are important, practical concepts that arise naturally in JavaScript code, so we'll learn about them now.
But, before we get to closures, let's review some ideas that you probably remember from prior CS classes, but I want to bring to the top of your mind.
Scope¶
In JavaScript, variables can be local or global. A global variable can be seen by any code anywhere; local variables can only be seen from a small part of the code. For example, consider the following code:
function square(n) {
return n * n;
}
var numSums = 0; // count how many times we use sumOfSquares
function sumOfSquares(n) {
numComputations ++; // increment the global counter
let total = 0;
let i = 0;
while( i <= n ) {
total += square(i);
i += 1;
}
return total;
}
The global variable numSums
is visible to both functions. (The
function names square
and sumOfSquares
also go into the global
namespace and so are visible to each other. That's how sumOfSquares
is able to invoke square
.)
The local variable n
is local to each function. Even though
sumOfSquares
called square
and they both use n
, there's no
conflict.
You can refresh your memory about global and local variables by following that link.
Other Scopes¶
There are other scopes in addition to local and global. We actually used these in the Plotting assignment without realizing it. Here's an example; see if you can figure out what it does:
function curve13(max, incr) {
var xvals = range(max);
var f = function (x) { return x+incr }; // closure
var yvals = xvals.map(f);
newPlot(xvals, yvals);
}
(What it does is plot the function f(x) = x+incr
for some value
incr
that is supplied to the curve13
function. For example,
curve13(10, 7)
plots x+7
for x in [0-9]. In general, curve13
plots a straight, 45 degree line, but one of a family of such lines,
depending on the value of its second argument, incr
.)
Focus on the f
function. What is the scope of x
? It's local to
f
, of course, since it's defined by the argument to f
.
What is the scope of incr
? The incr
variable is local to curve13
but it is non-local to f
, meaning that incr
is created and
assigned a value outside the definition of f
. In particular, the
incr
variable refers to the environment that curve13
created.
A variable that is non-global and non-local like incr
means that
f
is a closure. We say that f
is a "closure over incr
". We can
call incr
a closure variable.
Formally, a closure is a function bundled together with the
environment (that is, variables that are visible) that exists when the
function is created. For example, if we invoke curve(10,7)
the
closure combines f
with incr=7
, but if we instead invoke
curve13(10,2)
, the closure combines f
with incr=2
.
Closures as Return Values¶
If a closure is returned from a function, it can continue to refer to
its original environment. We did this in the Plotting assignment as well:
the functions returned from quadratic
and cubic
continued to refer to
their original environments, which is how they continued to know their
coefficient and roots.
Here is an example from the Plotting assignment:
function quadratic(coef, root1, root2) {
return function (x) { return coef*(x-root1)*(x-root2) };
}
var f1 = quadratic(1,2,3); // 1(x-2)(x-3)
var f2 = quadratic(2,3,5); // 2(x-3)(x-5)
f1(0); // returns 6
f2(0); // returns 30
The code works fine, but how? How does f1
remember the constants
1,2,3 that define it? How does f2
remember the constants 2, 3, 5
that define it? How can they use the same variables to store those
different values?
They can do these things because both f1
and f2
and any other
return value from quadratic
is in fact a closure that combines the
anonymous function with its environment, and that environment contains
coef
, root1
, and root2
. The variables coef
, root1
, and
root2
are closure variables. Those variables are not global
variables (they are local to quadratic
) but they are not local to
the function returned by quadratic
. They are non-local, non-global
variables.
Visualization¶
We will not discuss how closures are implemented, but the necessary features are clear:
- separate storage locations
- with names determined by its "birth" environment
- accessible only from the closure.
In this section, I'll present a some graphics that might help. If you come up with your own visualization that helps, by all means use that!
The following is a variation on the quadratic function. The line
function returns one of family of simple linear functions, differing
only by their slope. The return value is a closure.
function line(rise, run) {
let slope = rise/run;
let f = function (x) { return slope*x; };
return f;
}
Here it is again, with a colored box around the function and the closure variable that it refers to:
Imagine we invoke the line
function on some arguments, say 6 and 3,
and get a function (a closure) back with a slope of 2:
slope2 = line(6,3);
We might picture the return value, slope2
like this:
You can now imagine mapping that closure across some x-values to plot the function, just like we did in the Plotting assignment:
let xValues = range(20);
addPlotFunction(xValues, slope2);
I find it useful to imagine a closure as a small box containing the function and whatever closure variables it needs.
Closures as Event Handlers¶
You used closures without realizing it in the Plotting assignment. We also used closures without realizing it in Ottergram. The fact that you can implicitly use closures demonstrates that they are actually fairly intuitive. The reason it's important to be more explicit about them is on the occasions when they don't work, or when you modify your code in a way that you think is harmless, but turns out to break it.
Let's start with the working code from Ottergram:
function addThumbClickHandler(thumb) {
$(thumb).one().click(function (event) {
event.preventDefault();
setDetailsFromThumb(thumb);
});
}
Three fairly simple lines of code. This function creates a local, anonymous function and attaches it as an event listener to the given thumbnail that is the argument to the function.
We can visualize that function like this, where the value of thumb
is one of the clickable thumbnail hyperlinks from interactive
Ottergram. Here is one of the closures,
the one for Barry.
Note that the code of the function is just the following. It doesn't include any of the attachment code, because the closure is this inner function, not the code that attaches it.
function (event) {
event.preventDefault();
setDetailsFromThumb(thumb);
}
That event handler is attached to Barry:
Then, when the browser executes the handler, the handler is able to remember the thumbnail that it works with.
Misconceptions¶
I've said you can use a named function in place of an anonymous function if you're feeling uncomfortable with the anonymous function. Suppose you took me at my word and you did this:
function clickFunction(event) {
event.preventDefault();
setDetailsFromThumb(thumb);
});
function addThumbClickHandler(thumb) {
$(thumb).one().click(clickFunction);
}
The code for clickFunction
is identical to the code of the anonymous
function. All we did is gave it a name and used the name instead of
the function code. So, that should work, right? Wrong!
No, we also changed the environment of the function
definition. In the original code, the definition of the event handler
was in an environment (namespace) where the variable thumb
had a
meaning. We ripped the function out of its environment, and defined it
in the "top-level" environment (essentially, the environment where
global functions and variables live). But thumb
has no meaning in
the top-level environment, so when we run this code, we'll get an
error saying that thumb
is not defined, because thumb
has no
meaning inside clickFunction
.
Note that this is not an advantage of anonymous functions over named functions. It's just that with an anonymous function, we'll naturally keep the function definition in the correct environment, which is what is necessary.
Again, the problem is not the fact that we used a named function rather than an anonymous one. It's the change in environment (namespace). For example, the following re-write works fine:
function addThumbClickHandler(thumb) {
function clickFunction(event) {
event.preventDefault();
setDetailsFromThumb(thumb);
});
$(thumb).one().click(clickFunction);
}
Now the clickFunction
is defined in an environment (namespace) where
thumb
has meaning (the argument to addThumbClickHandler
), and all
is well.
If you'd like another example, I created this shopping example
Summary¶
I've tried to emphasize an intuitive understanding of closures, rather than emphasizing a formal definition. Still, that can be helpful.
Formally, a closure is a function plus an environment (you can read more about them in the Wikipedia article on Closures).
- Closures are functions that refer to non-local, non-global names (variables)
- Closures are defined in environments (namespaces) other than the "top-level" environment
- The non-local, non-global names are called closure variables.
- Closures can continue to refer to these closure variables forever.
- Closures are often anonymous function expressions, but that's not necessary.