Getting Closure(s)
Ezra Schwepker
Posted on June 18, 2019
What is a Closure?
A function that references bindings from local scopes around it is called a closure. Eloquent JavaScript
A simple definition, but not one that provides understanding without greater context.
A closure is a persistent scope which holds on to local variables even after the code execution has moved out of that block. Languages which support closure (such as JavaScript, Swift, and Ruby) will allow you to keep a reference to a scope (including its parent scopes), even after the block in which those variables were declared has finished executing, provided you keep a reference to that block or function somewhere. - StackOverflow
A longer definition, but still not that informative.
The first time I encountered a closure in use, I sat there, wondering what the hell just happened. It was like magic. I didn't know how it worked, just that it did.
And it seems that that is a common sentiment.
Luckily, they're actually quite simple. They're a solution to a problem. Once you see the problem, then you'll recognize the solution, closures, for what they are.
But first we need to discuss the three pieces to the puzzle that make closures necessary.
Lexical Scope
In a programming language, scope is a set of rules which govern where a variable binding may be accessed. There are two forms, lexical and dynamic.
With dynamic scope, variable bindings are available in relation to where a function is invoked, while with lexical scope, where the binding is written is key.
const x = 5;
const printX = ( ) => console.log('The value of X is: ', x);
const dynamicScope = ( ) => {
const x = 100;
printX( ); // uses the x where it was called from
}
dynamicScope( ); //-> The value of X is 100
const lexicalScope = ( ) => {
const x = 100;
printX( ); // uses the x where it was written
}
lexicalScope( ); //-> The value of X is 5
Lexical scope rules are the most common scoping system as they are easy to read and debug. The code that you write will behave consistently based upon how you defined it, not upon where it is used.
Lexical scoping produces a nested series of blocks which prevent a variable defined within a block from being accessed from outside of it.
// global scope
const a = 'outer';
const b = 'outer';
const c = 'outer';
{ // block scope
const b = 'inner';
const c = 'inner';
{ // nested block scope
const c = 'innermost';
console.log('InnerMost Scope: ', 'a: ', a, 'b: ', b, 'c: ', c);
//-> InnerMost Scope: a: outer, b: inner, c: innermost
}
console.log('Inner Scope: ', 'a: ', a, 'b: ', b, 'c: ', c);
//-> Inner Scope: a: outer, b: inner, c: inner
}
console.log('Outer Scope', 'a: ', a, 'b: ', b, 'c: ', c);
//-> Outer Scope: a: outer, b: outer, c: outer
When the innermost console.log
asks for the values of a
, b
, and c
, it first looks within the block in which it is defined. If it doesn't find the variable binding, it then looks in the block surrounding the block that it was defined within, and so on until it reaches the global scope and can go no further.
That means that each console.log
accesses the value of the variable in the scope where it was defined, or higher. The inner and outer scopes cannot see the value of the innermost scope.
When we define a function, it has it's own block scope, and the variables defined within it cannot be accessed from outside of the function.
function hasItsOwnScope() {
const innerScope = 'cannot access outside of function';
}
console.log(innerScope);
//-> Uncaught ReferenceError: innerScope is not defined
Execution Context
The next piece of the puzzle is Execution Context. Every time a function is called (aka executed, or invoked), the function is added to the call stack. If that function calls another function, then that function is added to the call stack, on top of the previous function. When a function is finished, it is removed from the call stack.
function first ( ) {
function second ( ) {
function third ( ) {
}
third( );
}
second( );
}
first( );
// Call stack: [ ]
// Call stack: [first]
// Call stack: [first, second]
// Call stack: [first, second, third]
// Call stack: [first, second]
// Call stack: [first]
// Call stack: [ ]
In order to conserve memory, the variables defined inside of a function are discarded when the function is removed from the call stack. Each time you call a function, it is a clean slate. Every variable defined within it, including parameters, is defined again.
These bindings, as well as special bindings available only inside of functions like arguments
, name
and caller
are stored in the Execution Context which contains all of the information the function needs to access the values of variables defined within it, as well as variables further up the lexical scope chain.
First Class & Higher Order Functions
Many languages these days allow for first-class functions, which means that you can treat a function like any other value. It can be bound to a variable definition:
const firstClass = function myFirstClassFn( ) { /* ... */ }
And it can be passed to functions as arguments, as well are returned by other functions. When a function accepts a function as an argument, or returns it, that function is called a Higher Order function:
function higherOrderFn(firstClassFnParameter) {
firstClassFnParameter( );
return function anotherFirstClassFn( ) { /* ... */ }
}
higherOrderFn(firstClass); //-> function anotherFirstClassFn...
The Problem
- We cannot access the values inside of a function from outside of a function
- The variables inside of a function only exist when the function is called
- But, we can define a function inside another function and return it.
So what happens when the returned first-class function tries to access a value defined inside of the returning higher-order function?
function higherOrder( ) {
const insideScope = "cannot be accessed outside";
return function firstClass( ) {
console.log(insideScope);
}
}
const returnedFn = higherOrder( );
returnedFn( ); //-> ???????
And THAT is a closure! Closures preserve the execution context of a function when another function is returned. The language knows that you might need the execution context later, so instead of discarding it, it attaches it to the returned function.
Later on when you're ready to use the returned function, it is able to access all of the values it needs, just like it would have been able to if you called it while it was still inside of the function you returned it from.
This is an incredibly powerful idea! You're now able to define private variables:
function higherOrder( ) {
let privateVariable = 'private';
return {
get: () => privateVariable,
set: (val) => privateVariable = val
}
}
console.log(privateVariable);
//-> Uncaught ReferenceError: privateVariable is not defined
const getterSetter = higherOrder( );
getterSetter.get( ); //-> 'private';
getterSetter.set('new value');
getterSetter.get( ); //-> 'new value'
You can also compose functions!
const log = function (message) {
return function (val) {
console.log(message, val);
}
}
const logWarning = log('Warning! We encountered an issue at: ');
const logError = log('Error: ');
logWarning('ChatBot message delivery');
logWarning('PostIt note stickyness');
logError('Connection lost');
While that is a simple example, the power to extend it is incredible. Functions are now stateful. A function returned by another function retains a memory of its higher order function and you can use that to combine functions like legos.
Posted on June 18, 2019
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.