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:

line function

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:

slope2, which is a closure

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.

one ottergram event handler

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:

attaching the handler

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.