Closure

Closure is not a part of JavaScript specification that is defined by ECMAScript. It is rather a phenomenon common to programming languages that have characteristics of functional programming. Therefore, definitions of Closure vary. After consulting various resources that explain what Closure is in their own terms, I've summed up Closure as follows:

Closure refers to a phenomenon where a function remembers the lexical context of its outer function even after the outer function's execution context has been removed from the call stack.

Now, let's get into details what each word means.

Execution Context

First, a quick recap of 🔗Execution Context would help. Let's take the following code snippet:

const outer = function () {
	var a = 1;
	var inner = function () {
		console.log(++a);
	};
	inner();
};

// console.log(a) // ReferenceError: a is not defined

outer(); // 2

// console.log(a) // ReferenceError: a is not defined

And here's rough visualization of how JavaScript engine executes the code by pushing and popping off stack:

[Figure1]

Couple of things to note:

  • When inner is invoked and it has to increment a's value by 1, a is not contained in inner's own environment record. However, via scope chain, as we can see in step 4., inner's execution context has reference to its outer execution context's environment record, which is that of outer. As a consequence, inner() can access and modify outer's function-scoped variable a  and the code runs without breaking.
  • Execution context of inner and outer is removed from call stack in order as they finishes execution.

Closure

In order to understand Closure in line with execution context, let's imagine the following situation:

What happens when the execution of outer is removed before inner's? Let's change the above code snippet so that outer returns not the returned value of inner but inner function itself.

const outer = function () {
	let a = 1
	const inner = function () {
		return ++a
	}
	return inner
}

const myInner = outer()
console.log(myInner())  // 2
console.log(myInner())  // 3

As banal as the code might appear, its mechanics is not comprehensible with our current knowledge of execution context. Here, the corresponding visual model for the code:

[Figure2]

Here, let's fasten on step 3. and 4. that,

  • by the time we invoke myInner, execution context of outer is already removed from call stack and there seems no way for myInner to access and modify a.
  • nonetheless, code ran without throwing any error.

The way JavaScript Garbage Collector works prevents code from breaking. Garbage Collector decides what to garbage-collect based on reference count. If a value is not referenced anywhere in the code, its reference count is 0 and should be released from memory In the above code, however, outer returns a function inner, which refers to a . This means that there's a possibility for a to be referenced in the future, even  outer  is popped off from call stack. inner creates a sort of closure over outer and  a is not marked to be garbage-collected.

Here is the revised visual model:

[Figure3]

To summarize, we call Closure a situation where a reference to a function-scoped variable from its inner function makes the variable accessible even after function is removed from call stack.

If we add console.dir(inner) to inner before return ++a, we see that  inner has hidden property [[Scopes]] , which has Closure scope, apart from Script scope and Global scope.

Screen Shot 2022-11-28 at 10.48.59 PM.png
[Figure4]

Best Practice

As we see in Figure3, outer's environment record { a : 1 } persists after the function has executed. While Closure intentionally prevents function-scoped variable from being garbage-collected, it would save us some memory if we release variable from memory when we are done using myInner. We can do so by making it explicit that myInner is not referenced anywhere and, as a consequence, setting a's reference count to 0. In the below code, we've achieved it by adding innerFunc = null to the last line. We replace myInner's value with primitive data type, instead of reference data type. Theoretically, we could have set it to any other primitive data type, such as undefined, but I guess null makes our intention most clear.

const outer = function () {
	let a = 1
	const inner = function () {
		return ++a
	}
	return inner
}

const myInner = outer()
console.log(myInner())  // 2
console.log(myInner())  // 3
myInner = null

new Function Syntax

While most functions, when nested, remember lexical context of its closest outer function, there is one exception. Those are functions created by new Function syntax. new Function() takes function body and parameters as string type arguments and generates function out of them. When unable to find variable in its own environment record, they are set to always reference global environment record, not its closest outer environment record.

Let's refactor the above code using new Function().

const outer = function () {
  let a = 1
  const inner = new Function('a++')
  return inner
}

const myInner = outer()
console.log(myInner())  // Uncaught ReferenceErrors: a is not defined at eval

As we see, function-scoped variable a is not visible to inner and therefore error is thrown. What if a were global variable?

let a = 1
const inner = new Function('a++')
inner()
console.log(a) // 2

This works totally fine.

Then what is the rationale for preventing new Function() construct from accessing and function-scoped variable?

  • First, new Function() creates function dynamically and therefore allowing its access to a function-scoped variable might expose the source code to malicious code injection.
  • Also, new Function()'s reference to local variable is prone to errors. JavaScript code is compressed by minifier when shipped for production. Minifier reduces size of source code by removing spaces, comments and replacing local variables' name with with shorter ones. Therefore, a long, human-centric variable name like const numOfComputersInStock = 100 can be replaced by const a = 100, while the string provided to new Function()(e.g. new Function(console.log(numOfComputersInStock))remains unchanged during compile time. Such behavior could cause errors. It is therefore recommended that local variables should be passed to new Function() as arguments, rather than being accessed directly.
const logNum = new Function('num', 'console.log(num)')

const numOfComputersInStock = 100
logNum(numOfComputersInStock) // 100
ℹ️
According to an article in A List Apart, "variable name replacement occurs only on local variables." It is because changing global variables affect not only variables developer created but also global objects such as window and document.

Motivation

Closure is a good example of encapsulation. With help of Closure, we can hide private data from overwriting and only expose to public what we want. By doing so, Closure lets us achieve something similar to access modifiers such as public, private, protected in Class, which makes Closure important not only to functional programming paradigms but also to object-oriented programming.

Let's take the following code for example.

const makeCounter = function () {
  let count = 1
  
  const increment = function() {
    count++
  }
  
  const decrement = function() {
    count--
  }
    
  return {
    get count () {
      return count
    },
    increment,
    decrement
  }
}

const myCounter = makeCounter();

myCounter.count // 1
myCounter.increment()
myCounter.increment()
myCounter.increment()
myCounter.decrement()
myCounter.getCount() // 3

myCounter.count = -100
myCounter.getCount() // 3

To achieve encapsulation, we've

  1. put in return statement what we want to expose as public members of myCounter module
  2. and kept it to function-scope what we want as private members.

In the above code, though incomplete (e.g. setting value to count directly doesn't throw an error), myCounter exposes count as a read-only getter and requires that the user of myCounter module access and modify myCounter via designated APIs(increment, decrement). Otherwise, direct assignment to count doesn't change the value of count.

Use Case

Module Pattern

Partially Applied Function

Currying

References

Subscribe to go-kahlo-lee

Don’t miss out on the latest issues. Sign up now to get access to the library of members-only issues.
jamie@example.com
Subscribe