loading...
Cover image for JS / explain closures

JS / explain closures

icncsx profile image DH Kim ・2 min read

A closure is a function with a referencing environment - basically a table of variables that the function has access to based on where it was originally defined. This behavior is also called lexical scoping where the accessibility of a variable is based solely on the function's position in the code - not where it was executed.

Let's have a look at an example. We're going to create an incrementing counter using a closure.

Increment Counter

const makeCounter = () => {
  let n = 0
  return () => {
    return ++n;
  }
}

const counter = makeCounter();
counter(); // 1
counter(); // 2

makeCounter is a function that returns another function that returns the incremented number. What's interesting about the returned function is that it has access to the n variable, despite it seemingly leaving the scope in which it was defined when it is assigned to a global variable called counter. This is the beauty of closures; functions access variables that are reachable in the function's original position in the code - not where it is executed, which is why lexical scope is also called static scope (scope don't shift around). A more fancy way of saying this: a function can access variables from its original referencing environment when invoked outside of their lexical scope. And the best part? You needn't do anything to use closures. Functions will always remember and access its lexical scope - that is if you are using a lexically scoped programming language (almost all modern programming languages).

To reaffirm the point, let's have a look at another example: a loaded die w/ incredibly suspicious sequencing.

Roll Loaded Die

const makeLoadedDie = () => {

  const sequence = [1, 2, 3, 4, 5, 6];
  let idx = 0;

  return () => {
    if (idx > sequence.length-1) {
      idx = 0;
    }
    return sequence[idx++]
  };
};

const rollLoadedDie = makeLoadedDie();
rollLoadedDie() // 1
rollLoadedDie() // 2

In this example, lexical scope will dictate that the inner function will have access to sequence and idx even if it is invoked outside of its immediate position. And sure enough when we do invoke the inner function in the global scope w/ rollLoadedDie(), the function remembers sequence and idx.

Whilst the most common pattern of closures is to call a parent function that returns an inner function, note that we can get the same behavior with an assignment.

let logVar;

const setup = () => {
  const x = 100;  
  logVar = () => {
    const y = 50;
    console.log(x + ', ' + y);
  }
}

setup()
logVar() // 100, 50

Well, that's pretty much it for closures today, and I hope you got the point: a function will not bother to look at the execution context. What matters is the lexical scope - the environment in which a function was defined. Nature over nurture I guess... or at least that's the analogy I used to describe closures to my non-tech friend who wanted to know what I was going to blog about. I love analogies by the way. If you have any good ones you've used to describe a difficult programming concept, please share them with me!

Warmly,
EK

Posted on May 28 by:

icncsx profile

DH Kim

@icncsx

Software engineer interested in cloud computing and serverless backend services.

Discussion

markdown guide