DEV Community

Cover image for Closure's wrap up aka "backpack"
Yvon Momboisse
Yvon Momboisse

Posted on

Closure's wrap up aka "backpack"

Closure seems to be perceived as some kind of mysterious concept and is probably one of the most dreaded of all JS interview questions. For some part of the dev community at least.

I am part of that part of the community. Since explaining sharpens understanding, then by way of an explanation I expect to clear some of that closure mystery.

Closure is a form of space division or partition. That is the way I conceive of it. Since partition of space comes with rules of inclusion and exclusion, knowing those rules will help grasp closure. Scope is the first thing to examine.

Scope

Scope can be looked at through two closely related angles. Scope as space and scope as rules.

Scope as space

MDN opens its definition of scope with the idea of space, in the form of context :

“The current context of execution. The context in which values and expressions are "visible" or can be referenced.”

Scope is an area where variables are visible, that is accessible to a function. As such, scope is a spatial relation between what can see and what can be seen. In other words, scope is a function’s visual field and that visual field is governed by rules.

Scope as rules

In the Scope and Closures volume of his You Don’t Know JS series, Kyle Sympson defines scope as the set of rules that governs the retrieval of variables in a computer program (Scope and Closures, 2014, p.11). Those rules guide both the compiler that produces executable code, and the programmer who writes the source code.

Declaring a function that references a variable stored in a place where the compiler hasn’t been told to search means program failure. It is up to the programmer to follow the protocol.

If the programmer cannot change the protocol, it is the way he decides to write his code that determines the units of scope. That prerogative of the author of code is called lexical scoping. When lexical scoping applies, scope is set relatively to where a function is declared. That’s the programmer’s choice. It is not the only way that scope is set and some languages use dynamic scoping, which sets scope based on where variables are declared. That is to say, when the compiler looks for a variable’s value, it searches for what has been assigned to it most recently.

let x = 2;
function foo(a) { return x + a }
function bar() { let x = 3; return foo(0) }
bar();
Enter fullscreen mode Exit fullscreen mode

With lexical scoping, bar() would evaluate to 2. If JS had dynamic scoping, it would evaluate to 3.

The rules of scope restrict the size the function’s visual field (or search area if we look at it through compiler eye). What is the visual field of a function made up of ? A function has access to its outer scope (including outer of outer etc) and its own inner scope, but not to other functions’ inner scopes (for example a sibling or child function, ie. the inner scope of functions that are either contained in outer scope or inner scope).

Going back to the previous program, foo() has access to x = 2, which is found in the outer scope. It does not however have access to x = 3, which is found in its sibling’s scope.

When looking for a variable the compiler always starts looking in the inner scope of the function. If search fails there, the compiler will look in the outer scope, if it fails there, it will go to the outer scope of the outer scope and all the way up to the global scope, if needed. If nothing is found there, search stops since the global scope does not have an outer scope.

With dynamic scoping, when foo() is executed and the compiler needs to get the value assigned to a variable name, it will look for the most recent value assigned to that variable, which is 3.

Now is a good time to bring back the idea of context. The word “context” is a synonym of “surrounding”, and the idea of surrounding is at the heart of closure.

Closure

Closure aliases

Closure has a few aliases, like Closed Over Variable Environment (C.O.V.E.), Persistent Lexical Scope Referenced Data (P.L.S.R.D.) or the “backpack” to name a few (Will Sentance coined the last one, and his workshops on Frontend Masters are incredibly useful and accessible).

Even though they refer to the same thing, all three aliases focus on a different angle of the concept. C.O.V.E. emphasizes the enclosing process at play in a closure, P.L.S.R.D. focuses on the persistence of data and “backpack” underlines the idea that stuff is being carried around.

That which is carried around is a variable environment or in other words, a piece of lexical scope. How does this happen ?

Closure as a bundle/backpack

As was said earlier the rules of scope mean that a function has access to variables in the outer scope and its own inner scope, as long as those scopes do not belong to other functions’ inner scopes. Closure is the thing that makes it possible for a function that is executing outside of its original lexical environment to access all the variables of that environment (Scope and Closures, 2014, p. 48). Making it seem as though some inner scope is accessed from outer scope. For MDN a closure can be conceived as the

“combination of a function bundled together (enclosed) with references to its surrounding state (the lexical environment)”

Moreover, to close over a variable environment and make it persistent, a function has to be returned.

Some examples

ReferenceError

function tellSecret() { return secret }
function hideSecret() { 
let secret = “I ate all the cake”; 
tellSecret(secret) 
}
hideSecret(); // ReferenceError: secret is not defined

Enter fullscreen mode Exit fullscreen mode

This is how you would expect things to work. Invoking hideSecret() throws a ReferenceError, since tellSecret(), which is called from the inner scope, references a variable secret as parameter that is nowhere to be found in its outer or inner scope. Sure that variable is sitting right next to it in hideSecret’s inner scope, but tellSecret does not have access to its sibling’s inner scope.

Truth comes out

function hideSecret() { 
  let secret = “I ate all the cake”; 
  return function needToSay() { 
    return secret; 
    }
 }
let tellSecret = hideSecret();
tellSecret(); // “I ate all the cake”
Enter fullscreen mode Exit fullscreen mode

When a function is executed, it is pushed onto the call stack and a new execution context is created. Within that execution context, variables are accessible following the rules of scope. When execution reaches a return statement or the bottom of the function, it is popped off the stack and the execution context is erased. The variable environment enclosed in the function’s inner scope vanishes. However, with closure, that variable environment persists. That is what happens above.

The return value of hideSecret() is assigned to a variable called tellSecret. That return value is needToSay’s function declaration. When slapping a pair of parentheses at the end of tellSecret, it is the code inside of needToSay that is being executed, bundled together with its lexical environment. The value of secret is being returned, which is nowhere to be found in global scope. Even if hideSecret has been popped off the call stack, by returning needToSay, a record has been made of that lexical environment, and that is closure.

One thing and the other

function tellSecret(cb) { 
  let secret = " I did NOT eat the cake"; 
  return cb(secret); 
}
function hideSecret() { 
  let secret = "I ate all the cake";
  function sayOneThing(a) { 
    return function sayAnother(b) { 
      return a + " " + b;
    }
  } 
  return tellSecret(sayOneThing(secret)); 
}
let s = hideSecret(); 
s(); // "I ate all the cake  I did NOT eat the cake"

Enter fullscreen mode Exit fullscreen mode

First tellSecret is declared, then hideSecret and then the return value of hideSecret is assigned to the variable s. What does hideSecret return ? It returns a call to tellSecret, with the function sayOneThing passed as parameter. So hideSecret should return whatever tellSecret returns. What does the call to tellSecret evaluate to ? The return value of tellSecret will be whatever the function that is passed as parameter returns. So tellSecret should return whatever sayOneThing returns. What does sayOneThing return ? The return value of sayOneThing is the definition of a function called sayAnother. So invoking s amounts to calling sayAnother, and sayAnother returns the concatenation of whatever parameter was passed in sayOneThing (“I ate all the cake”) and sayAnother (“I did NOT eat the cake”). It is because sayAnother is bundled with a record of sayOneThing’s lexical environment that it can return a variable from an execution context that looks as though it’s gone. That is what closure is.

I think.

Top comments (0)