Javascript Closures
Before getting started with learning closures, it's important to understand what scope and execution context are.
If you have not read the first two articles yet, the topic of closures will be very confusing. Make sure to read these first so the subject of closures makes sense.
How and When are Function Objects Created?
In javascript, functions are objects, which is why we can add properties to functions.
function foo() {
console.log('foo')
}
foo.bar = 'bar'
console.log(foo.bar) // bar
What makes functions or function objects special is they support the dynamic evaluation of code. This is possible because they have an internal [[Call]]
method which executes code associated with this object. This internal [[Call]]
method is invoked via a function call expression foo()
. When foo()
is invoked, the function object's internal [[Call]]
method is called and code executes, however the function object was already created before the function's code was executed.
In the above code block, the function is in global scope, which means this function will be created in GlobalDeclarationInstantiation
. This step is detailed on line 16.
GlobalDeclarationInstantiation
- For each Parse Node f of functionsToInitialize, do
- Let fn be the sole element of the BoundNames of f.
- Let fo be InstantiateFunctionObject of f with arguments env and privateEnv.
- Perform ? env.CreateGlobalFunctionBinding(fn, fo, false).
InstantiateFunctionObject
is an operation in the spec, but if we follow the proper productions we end up at OrdinaryFunctionCreate
.
OrdinaryFunctionCreate
This abstract operation returns a function object. Notice on line 2
, a regular object is created, but then it is extended with certain internal methods such as [[Call]]
. On line 13
, the [[Environment]]
internal slot is pointed to the Environment Record that the function closes over. In other words, [[Environment]]
is the Environment Record that the function was declared in, which also is the outer environment record when evaluating the code of the function. If that doesn't make sense, remember that the function is declared in the global scope, so its [[Environment]]
is the Global Environment Record or Global Scope, but when the function is invoked, a new Function Environment Record or Function Scope is created and the Function Environment Record's [[OuterEnv]]
field is pointing at the Global Environment Record.
- Let internalSlotsList be the internal slots listed in Table 30.
- Let F be OrdinaryObjectCreate(functionPrototype, internalSlotsList).
- Set F.[[Call]] to the definition specified in 10.2.1.
- Set F.[[SourceText]] to sourceText.
- Set F.[[FormalParameters]] to ParameterList.
- Set F.[[ECMAScriptCode]] to Body.
- If the source text matched by Body is strict mode code, let Strict be true; else let Strict be false.
- Set F.[[Strict]] to Strict.
- If thisMode is lexical-this, set F.[[ThisMode]] to lexical.
- Else if Strict is true, set F.[[ThisMode]] to strict.
- Else, set F.[[ThisMode]] to global.
- Set F.[[IsClassConstructor]] to false.
- Set F.[[Environment]] to env.
- Set F.[[PrivateEnvironment]] to privateEnv.
- Set F.[[ScriptOrModule]] to GetActiveScriptOrModule().
- Set F.[[Realm]] to the current Realm Record.
- Set F.[[HomeObject]] to undefined.
- Set F.[[Fields]] to a new empty List.
- Set F.[[PrivateMethods]] to a new empty List.
- Set F.[[ClassFieldInitializerName]] to empty.
- Let len be the ExpectedArgumentCount of ParameterList.
- Perform SetFunctionLength(F, len).
- Return F.
Example
In the below code block, we have a function in global scope. Any time any code is evaluated, there is always at least the global execution context. This is how the below code will be evaluated in this order:
- A global exeuction context will be created
- The
LexicalEnvironment
andVariableEnvironment
state components will be created and pointed to the global environment record. - The global execution context will be pushed onto the execution context stack and is now the running execution context.
- The function object
greet
will be created (OrdinaryFunctionCreate
). - A mutable binding will be created in the Object Environment Record for
greet
and will be initialized to its function object created in the previous step. This means the function is a property on the binding object, which iswindow
in the case of browsers if we are not instrict mode
. For this reason functions declared in global scope we can be called withgreet()
orwindow.greet()
. (CreateGlobalFunctionBinding
) - Code evaluation begins, and on
line 6
whengreet()
is evaluated, a new function execution context is created. - A new Function Environment Record or function scope is created with
NewFunctionEnvironment
.- The
[[OuterEnv]]
field of the Function Environment Record is set to the the value of the function object's[[Environment]]
internal slot, which is the Environment Record the function object was created in.
- The
- The
LexicalEnvironment
andVariableEnvironment
state components of the function execution context are created and pointed to the new function scope that was just created. - The running execution context will be suspended
- This new function execution context will be pushed onto the execution context stack and will become the running execution context.
- The value of the
this
keyword will be set - Evaluation of the functions code begins
const name
variable will be instantiated and set touninitialized
- Evaluation begins and on
line 2
name
will be initialized to'david'
- The function code evaluation finishes and the function execution context will be removed from the execution context stack.
Once the function execution context is removed from the call stack, the variable const name = 'david'
ceases to exist and will be garbage collected.
In conclusion, once the global execution context is established, the function object greet
will be created (step 4) with the OrdinaryFunctionCreate
abstract operation.
function greet() {
const name = 'david'
console.log(`hi ${name}`)
}
greet()
What are Closures?
The typical definition of a closure is a function bundled together with reference to its lexical environment, but this isn't entirely correct. What a closure really is, is the practice of keeping alive an instance of a function object and its referenced environment record (which could also form a chain if the the referenced environment record is a grandparent environment), by keeping at least one reference to that function object instance somewhere else in the program.
Note that each time greet()
is invoked, a new function environment record (function scope) is created, and a new instance of a greetInner
function object is created. This means if you call greet()
twice, two seperate function environment records are created, and two instances of the greetInner
function object are created, and each greetInner
function object's [[Environment]]
internal slot references the respective function environment record instance created when greet()
is called.
function greet() {
const name = 'david'
return function greetInner() {
console.log(`hi ${name}`)
}
}
// greetFn is a reference to greetInner
const greetFn = greet()
greetFn() // hi david
Here is another example but this is a bit different, notice how the greetInner
function closes over the name
parameter.
function greet(name) {
return function greetInner() {
console.log(`hi ${name}`)
}
}
const greetFn = greet('david')
greetFn() // hi david
Note that we said a closure is the practice of keeping alive a function object instance and its referenced environment record by keeping at least one reference to that function object. If no reference is kept, then once the function finishes execution, the variables cease to exist and get garbage collected. Below, we did not save a reference to greetInner
so once greet()
finishes execution, the returned function and name variable cease to exist.
function greet() {
const name = 'david'
return function greetInner() {
console.log(`hi ${name}`)
}
}
greet()
Each Closure has a Reference to its Own Environment Record
Earlier we said each instance of a function object maintains a reference to its own environment record instance through its [[Environment]]
internal slot, which is the environment record that function object was created in. This is because each time a function object is invoked, a new function execution context is created, and a new function environment record is created with the abstract operation NewFunctionEnvironment
. This new function environment record's [[OuterEnv]]
field is set to the executing function's [[Environment]]
value.
Each time a function is invoked, the whole process repeats itself, which means a new function environment record is created, and the variables are created again even if it is the same function that gets called.
In the below code, both counter1
and counter2
share the same function body definition, but they each store there own increment
and getValue
function object instances as well as their own environment records which is why each environment record updates independently of the other.
function counter() {
let count = 0
function increment() {
count++
}
function getValue() {
return count
}
return { increment, getValue }
}
const counter1 = counter()
const counter2 = counter()
// this updates counter1 environment record
counter1.getValue() // 0
counter1.increment()
counter1.getValue() // 1
// this updates counter2 environment record
counter2.getValue() // 0
counter2.increment()
counter2.increment()
counter2.getValue() // 2
// counter1 was not affected
counter1.getValue() // 1
Closures Close Over a Variable Not its Value
This is a common misconception, a closure closes over a variable, not its value. This means that when the below variable x
is updated, there is indeed still a closure over the variable x
, but it reflects the updated value of the variable.
let x = 1
const y = () => x
y() // 1
x++
y() // 2
x = 'foo'
y() // 'foo'
Closures in Loops
The below example was used when we talked about scope in an earlier article. Now this should be a bit more clear why it doesn't work. The arrow function getI = () => i
closes over i
in the initialization block. However, the way a for
loop works is that for each iteration, a new declarative environment record (scope) is created. The binding values from the last iteration are used to re-initialize the new variables which means for each iteration we have a new i
variable and the getI = () => i
function closed over the original i
variable from the initialization block.
for (let i = 0, getI = () => i; i < 3; i++) {
console.log(getI());
}
Block Scoped Closures
Closures can also close over block scoped variables.
function foo() {
const x = 1
const y = 1
{
const y = 2
return () => console.log(x, y)
}
}
const bar = foo()
console.dir(bar)
// Block (foo) {y: 2}
// Closure (foo) {x: 1}
Closures vs Lexical Scope
Closures do not occur in global scope because the function must be invoked from a parent scope. We know that inner functions have access to outer function scopes but outer functions do not have access to inner function scopes. If a variable is not found in the current scope then the js engine will look into the outer scope for the variable.
In the below example, there is no closure because global variables are always available anywhere, so no closure is formed.
const x = 1
function foo() {
return function bar() {
debugger
console.log(x)
}
}
const barFn = foo()
barFn()
In this example, when we call barFn()
it is in the global scope but we need a variable in the scope of foo
which is not available to us. This is why a closure is formed, because we created a reference to the inner function bar
from the global scope saved in a variable barFn
. The function bar
refrerences a variable const x = 1
in its outer environment record so that live variable is preserved.
function foo() {
const x = 1
return function bar() {
debugger
console.log(x)
}
}
const barFn = foo()
barFn() // Closure (foo) {x: 1}
Now that we understand that we will make another example that makes use of lexical scope lookup. Below when bar()
is called, there is no closure formed, the variable const x = 1
is not found in the scope of bar
so the js engine looks into the outer scope which is the scope of foo
.
function foo() {
const x = 1
bar()
function bar() {
console.log(x)
}
}
foo()
We can save a reference to an inner function, keeping that instance of a function object alive as well as its referenced environment record from within a function as long as that function is executing.
function a() {
function b() {
const x = 1
return function c() {
debugger // here is where the closure is formed
console.log(x)
}
}
const cFn = b()
cFn() // Closure (b) {x: 1}
} // here the closure does not exist anymore
a()
Lexical scope is just the typical behavior we would expect, when a variable is not found in the current scope then the js engine looks into the outer scope and checks if the variable can be found there. Closures allow us to hold on to an instance of a function object and its referenced environment record, invoking it from parent scopes.
Do Closures Close Over Scope Or Over Variables
Since a closure is the practice of keeping alive an instance of a function object and it's referenced environment record, the spec does not say anything about it being per variable, in fact it tells us that the [[Environment]]
internal slot is:
the Environment Record that the function was closed over. Used as the outer environment when evaluating the code of the function.
But in the below code block, if we had a massive module, it would eat up a lot of memory if we had one small closure formed and many other functions and variables also had to be stored in memory.
function SomeHugeModule() {
/*
pretend many variables
and function declarations
are in this scope
*/
const x = 1
return function getX() {
return x
}
}
What actually happens is closures close over an environment record and then the modern javascript engines as an additional optimization step trims down the environment record to only what was closed over. This can be observed using a debugger
statement which will indeed show this is the case.
This small example can be pasted into chrome dev tools and notice in the scope pane there is no closure over the variable const x = 'x'
. There is a closure over each environment record, but then the js engine trims that environment record down to only the variables it needs in order to save memory. In the scope of function b() {/* */}
we have three variables but only y
and z
are closed over.
function a() {
const w = 'w'
return function b() {
const x = 'x'
const y = 'y'
const z = 'z'
return function c() {
debugger
console.log(w, y, z)
}
}
}
const cFn = a()()
cFn()
// Closure (b) {y: 'y', z: 'z'}
// Closure (a) {w: 'w'}
Closures With Event Listeners
Earlier we said the a closure is the practice of keeping alive an instance of a function object and its referenced environment record (which could also form a chain if the the referenced environment record is a grandparent environment), by keeping at least one reference to that function object instance somewhere else in the program.
This example is different because a closure is formed even though setupCounter
does not return the inner setCounter
function. The reason this code still works is because the event listener is attached to an element and therefore a reference to it is maintained, each time we click on the button, its event listener callback is being called in place and therefore it has access to its surrounding scope.
// somewhere on the index.html page
// <button id="counter"></button>
function setupCounter(element) {
let counter = 0
const setCounter = (count) => {
counter = count
element.innerHTML = `count is ${counter}`
}
element.addEventListener('click', () => {
setCounter(counter + 1)
})
setCounter(0)
}
setupCounter(document.querySelector('#counter'))
This is extremely useful because we might have two buttons, and each one maintains its own state. Here we are able to use the same function setupCounter
but each button will hold a reference to its own environment record (scope) instance and therefore each one will update independently from the other.
// somewhere on the index.html page
// <button id="counter"></button>
// <button id="counter2"></button>
function setupCounter(element) {
let counter = 0
const setCounter = (count) => {
counter = count
element.innerHTML = `count is ${counter}`
}
element.addEventListener('click', () => {
setCounter(counter + 1)
})
setCounter(0)
}
setupCounter(document.querySelector('#counter'))
setupCounter(document.querySelector('#counter2'))
Closures Advanced
Below we are using a technique called currying along with the implicit return feature of arrow functions which is why we can write everything on one line.
Pay attention to the comments and notice how each time a function is invoked with an argument, a new closure is formed over the argument.
const sum = a => b => c => d => a + b + c + d
/**
* calls a => b => c => d => a + b + c + d
* returns b => c => d => a + b + c + d
* closes over variable `a`
*/
const sumFn = sum(1)
console.dir(sumFn) // closure {a: 1}
/**
* calls b => c => d => a + b + c + d
* returns c => d => a + b + c + d
* closes over variables `b`, `a`
*/
const sumFn = sum(1)(2)
console.dir(sumFn) // closure {b: 2} closure {a: 1}
/**
* calls c => d => a + b + c + d
* returns d => a + b + c + d
* closes over variables `c`, `b`, `a`
*/
const sumFn = sum(1)(2)(3)
console.dir(sumFn) // closure {c: 3} closure {b: 2} closure {a: 1}
/**
* calls d => a + b + c + d
* returns 10
*/
const returnVal = sum(1)(2)(3)(4)
console.dir(returnVal) // 10
A parameter is a variable in a function declaration, an argument is a value of this variable that gets passed to the function at function invocation time. In the below code block, the last function d => a
closes over the variable a
. When foo(1)
gets called, a new function execution context is created, a new function environment record is created, and b => c => d => a
is returned and stored inside the bar
variable.
But what is also stored inside the bar
variable is the function environment record that was created when foo(1)
was called.
A closure is formed because the returned function references the a
parameter.
const foo = a => b => c => d => a
/**
* here a closure is formed
* 1 was passed as `a` and it is
* referenced by the function
* d => a
*/
const bar = foo(1)
console.dir(bar) // closure {a: 1}
The example below is almost the same as the above example, but this time the last function d => c
returns c
not a
. When foo(1)
is called, 1
is bound to the parameter a
and b => c => d => c
is returned but there is no reference to a
and therefore no closure. The same thing happens with foo(1)(2)
, 1
is bound to a
, 2
is bound to b
and c => d => c
is returned but there are no references to a
or b
so no closure is formed.
const foo = a => b => c => d => c
/**
* calls a => b => c => d => c
* returns b => c => d => c
* `a` is not referenced
* there is no closure
*/
const bar = foo(1)
console.dir(bar)
/**
* calls b => c => d => c
* returns c => d => c
* `a` and `b` are not referenced
* there is no closure
*/
const bar = foo(1)(2)
console.dir(bar)
/**
* calls c => d => c
* returns d => c
* `c` is referenced
* now there is a closure
*/
const bar = foo(1)(2)(3)
console.dir(bar) // closure {c: 3}
Review
- Closure is a combination of an instance of a function object and the environment record that function object references (this could also form a chain if a reference is made to a grandparent environment record) as long as at least one reference is kept to that function object instance somewhere in the program.
- Closures can close over block scoped variables
- Closures close over a variable, not the value the variable holds
- A closure over a variable exists as long as there is a reference to that function.