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:
Couple of things to note:
- When
inner
is invoked and it has to incrementa
's value by 1,a
is not contained ininner
'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 ofouter
. As a consequence,inner()
can access and modifyouter
's function-scoped variablea
and the code runs without breaking. - Execution context of
inner
andouter
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:
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 formyInner
to access and modifya
. - 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:
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.
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 likeconst numOfComputersInStock = 100
can be replaced byconst a = 100
, while the string provided tonew 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 tonew Function()
as arguments, rather than being accessed directly.
const logNum = new Function('num', 'console.log(num)')
const numOfComputersInStock = 100
logNum(numOfComputersInStock) // 100
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
- put in
return
statement what we want to expose as public members ofmyCounter
module - 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
- 코어 자바스크립트(Core JavaScript), 재남, 정. 위키북스, 2019. pp. 115-145.
- "The "Variable scope, closure", The Modern JavaScript Tutorial
- "The "new Function" syntax", The Modern JavaScript Tutorial
- "JavaScript Minification II", A List Apart