JavaScript Closures

A closure is the combination of a function object and a retained environment record.

Closures are a powerful feature of JavaScript, and like many JavaScript features, they're often misunderstood due to lack of understanding of how the JavaScript engine executes code.

In order to understand this article it's important to first read about how the JavaScript engine executes code.

Execution Context

To briefly review, the JavaScript engine uses execution contexts to track the runtime evaluation of code. An execution context is created whenever a function is invoked or code is evaluated in global scope.

Execution contexts are tracked using the execution context stack, commonly referred to as the callstack. Each entry in the callstack represents an execution context, and the topmost entry is referred to by the specification as the running execution context. At any point during execution, the running execution context may be suspended and a different execution context will become the running execution context.

Once an execution context is created and pushed onto the execution context stack, the engine performs declaration instantiation. This is where function declaration bindings are initialized to their function objects, var bindings are initialized to undefined, and let and const bindings are created but remain uninitialized until their declarations are evaluated.

When evaluation reaches a call expression such as foo(), the function call will be evaluated, which calls the function objects internal [[Call]] method. This creates a new function execution context and a new Function Environment Record.

The new function environment record has it's [[OuterEnv]] property set to the value of the [[Environment]] property of the function object for which it's created.

What is a Closure?

Function objects are created during declaration instantiation. When a function object is created, its [[Environment]] internal slot holds a reference to the environment record in which it was defined.

Normally, the only reference to a function’s environment record is held by the function’s execution context. When that execution context is removed from the execution context stack, the environment record becomes unreachable and can be garbage collected.

If a function object remains reachable, the environment record referenced by its [[Environment]] internal slot remains reachable as well and cannot be garbage collected.

The function object together with the environment record referenced by its [[Environment]] internal slot forms a closure.

An internal slot is spec defined internal state associated with an object that ECMAScript algorithms use to describe behavior. It is not a property and cannot be accessed directly from JavaScript code.
The term close over is often used when describing closures. In the code below, greetInner closes over (or captures) the greet function environment record.

When greet finishes execution, the greetInner function object is returned and the innerFn binding is initialized with that value. Even though the function execution context for greet() has been removed from the execution context stack, a reference to the greetInner function object still exists.

JavaScript
function greet() {
  const name = 'david'

  return function greetInner() {
    console.log(`hi ${name}`)
  }
}

const innerFn = greet()
innerFn()

When greet('david') finishes execution, the greetInner function object is returned and the innerFn binding is initialized with that value. The greetInner function object closes over the environment record containing the name formal parameter binding.

JavaScript
function greet(name) {
  return function greetInner() {
    console.log(`hi ${name}`)
  }
}

const innerFn = greet('david')
innerFn()

If there are no default value initializers for the function parameters, then the function body declarations are instantiated in the same environment record as the parameters.

function greet(name) {
  return function greetInner() {
    console.log(`hi ${name}`)
  }
}

const innerFn = greet('david')
innerFn()
GreetExecutionContext = {
  LexicalEnvironment: GreetEnvironmentRecord,
  VariableEnvironment: GreetEnvironmentRecord
}

GreetEnvironmentRecord = {
  type: FunctionEnvironmentRecord,
  Bindings: {
    name: { [[Value]]: "david" },
    greetInner: { [[Value]]: <greetInner function object> }
  },
  [[ThisValue]]: GlobalExecutionContext.[[GlobalThisValue]],
  [[ThisBindingStatus]]: INITIALIZED,
  [[FunctionObject]]: <greet function object>,
  [[NewTarget]]: undefined,
  [[OuterEnv]]: <greet function object>.[[Environment]]
}

<greetInner function object> = {
  [[Call]]: <internal method>,
  [[Environment]]: GreetEnvironmentRecord,
  [[ThisMode]]: GLOBAL
}

<greet function object> = {
  [[Call]]: <internal method>,
  [[Environment]]: GlobalEnvironmentRecord,
  [[ThisMode]]: GLOBAL
}

If default value parameter initializers exist, a second environment record is created for the body declarations.

function greet(name = 'john') {
  return function greetInner() {
    console.log(`hi ${name}`)
  }
}

const innerFn = greet()
innerFn()
GreetExecutionContext = {
  LexicalEnvironment: GreetBodyEnvironmentRecord,
  VariableEnvironment: GreetBodyEnvironmentRecord
}

GreetBodyEnvironmentRecord = {
  type: DeclarativeEnvironmentRecord,
  Bindings: {
    greetInner: { [[Value]]: <greetInner function object> }
  },
  [[OuterEnv]]: GreetEnvironmentRecord
}

GreetEnvironmentRecord = {
  type: FunctionEnvironmentRecord,
  Bindings: {
    name: { [[Value]]: "john" }
  },
  [[ThisValue]]: GlobalExecutionContext.[[GlobalThisValue]],
  [[ThisBindingStatus]]: INITIALIZED,
  [[FunctionObject]]: <greet function object>,
  [[NewTarget]]: undefined,
  [[OuterEnv]]: <greet function object>.[[Environment]]
}

<greetInner function object> = {
  [[Call]]: <internal method>,
  [[Environment]]: GreetBodyEnvironmentRecord,
  [[ThisMode]]: GLOBAL
}

<greet function object> = {
  [[Call]]: <internal method>,
  [[Environment]]: GlobalEnvironmentRecord,
  [[ThisMode]]: GLOBAL
}

A separate environment record is created so that expressions evaluated in the formal parameter list do not have visibility of bindings declared in the function body.

function foo(a = () => b) {
  let b = 1
}

If a separate environment record were not created for formal parameter default initializers, the above code would behave like the following, which is not the intended behavior.

function foo() {
  let a = () => b
  let b = 1
}

Example

At this point, the compilation phase is finished and executable code has been generated. The JavaScript engine will create an execution context for this block of code.

function greet() {
  const name = 'david'

  return function greetInner() {
    console.log(`hi ${name}`)
  }
}

const innerFn = greet()
innerFn()

The JavaScript engine creates a new execution context along with a new environment record and pushes it onto the execution context stack. GlobalDeclarationInstantiation is performed, initializing bindings in that environment record. Function declarations are initialized to function objects, var bindings are initialized to undefined, and let and const bindings are created but remain uninitialized until their declarations are evaluated.

It's important to note that function objects have an internal [[Environment]] property which holds a reference to the environment record in which they are defined. In this case, the greet function is defined in the global environment record.
GlobalExecutionContext = {
  LexicalEnvironment: GlobalEnvironmentRecord,
  VariableEnvironment: GlobalEnvironmentRecord
}

<greet function object> = {
  [[Call]]: <internal method>,
  [[Environment]]: GlobalEnvironmentRecord,
  [[ThisMode]]: GLOBAL
}

GlobalEnvironmentRecord = {
  Type: GlobalEnvironmentRecord,
  [[ObjectRecord]]: {
    Type: ObjectEnvironmentRecord,
    [[BindingObject]]: <global object>, // e.g., window in browsers
    Bindings: {
      // Built-in globals (e.g., Math, Array, JSON, etc.)
      greet: { [[Value]]: <greet function object>
    },
    [[OuterEnv]]: null
  },
  [[DeclarativeRecord]]: {
    Type: DeclarativeEnvironmentRecord,
    Bindings: {
      innerFn: { [[Value]]: uninitialized }
    },
    [[OuterEnv]]: null
  },
  [[GlobalThisValue]]: <global object>,
  [[OuterEnv]]: null
}

When evaluation reaches the call expression, the greet function object's internal [[Call]] method is invoked. A new function execution context and a new function environment record are created and pushed onto the execution context stack. FunctionDeclarationInstantiation is performed, creating and initializing bindings in that function environment record.

Note that innerFn remains uninitialized.

When a function environment record is created, its [[OuterEnv]] is set to the value of the [[Environment]] property of the function object for which the environment record is created.

function greet() {
  const name = 'david'

  return function greetInner() {
    console.log(`hi ${name}`)
  }
}

const innerFn = greet()
innerFn()
GreetExecutionContext = {
  LexicalEnvironment: GreetEnvironmentRecord
  VariableEnvironment: GreetEnvironmentRecord
}

GreetEnvironmentRecord = {
  type: FunctionEnvironmentRecord
  Bindings: {
    name: { [[Value]]: uninitialized },
    greetInner: { [[Value]]: <greetInner function object> }
  },
  [[ThisValue]]: GlobalEnvironmentRecord.[[GlobalThisValue]],
  [[ThisBindingStatus]]: INITIALIZED,
  [[FunctionObject]]: <greet function object>,
  [[NewTarget]]: undefined,
  [[OuterEnv]]: <greet function object>.[[Environment]]
}

<greetInner function object> = {
  [[Call]]: <internal method>,
  [[Environment]]: GreetEnvironmentRecord,
  [[ThisMode]]: GLOBAL
}

GlobalExecutionContext = {
  LexicalEnvironment: GlobalEnvironmentRecord,
  VariableEnvironment: GlobalEnvironmentRecord
}

<greet function object> = {
  [[Call]]: <internal method>,
  [[Environment]]: GlobalEnvironmentRecord,
  [[ThisMode]]: GLOBAL
}

GlobalEnvironmentRecord = {
  Type: GlobalEnvironmentRecord,
  [[ObjectRecord]]: {
    Type: ObjectEnvironmentRecord,
    [[BindingObject]]: <global object>, // e.g., window in browsers
    Bindings: {
      // Built-in globals (e.g., Math, Array, JSON, etc.)
      greet: { [[Value]]: <greet function object>
    },
    [[OuterEnv]]: null
  },
  [[DeclarativeRecord]]: {
    Type: DeclarativeEnvironmentRecord,
    Bindings: {
      innerFn: { [[Value]]: uninitialized }
    },
    [[OuterEnv]]: null
  },
  [[GlobalThisValue]]: <global object>,
  [[OuterEnv]]: null
}

After the bindings have been initialized from FunctionDeclarationInstantiation, the engine evaluates the statement list associated with that function execution context.

function greet() {
  const name = 'david'

  return function greetInner() {
    console.log(`hi ${name}`)
  }
}

const innerFn = greet()
innerFn()
GreetExecutionContext = {
  LexicalEnvironment: GreetEnvironmentRecord
  VariableEnvironment: GreetEnvironmentRecord
}

GreetEnvironmentRecord = {
  type: FunctionEnvironmentRecord
  Bindings: {
    name: { [[Value]]: 'david' },
    greetInner: { [[Value]]: <greetInner function object> }
  },
  [[ThisValue]]: GlobalEnvironmentRecord.[[GlobalThisValue]],
  [[ThisBindingStatus]]: INITIALIZED,
  [[FunctionObject]]: <greet function object>,
  [[NewTarget]]: undefined,
  [[OuterEnv]]: <greet function object>.[[Environment]]
}

<greetInner function object> = {
  [[Call]]: <internal method>,
  [[Environment]]: GreetEnvironmentRecord,
  [[ThisMode]]: GLOBAL
}

GlobalExecutionContext = {
  LexicalEnvironment: GlobalEnvironmentRecord,
  VariableEnvironment: GlobalEnvironmentRecord
}

<greet function object> = {
  [[Call]]: <internal method>,
  [[Environment]]: GlobalEnvironmentRecord,
  [[ThisMode]]: GLOBAL
}

GlobalEnvironmentRecord = {
  Type: GlobalEnvironmentRecord,
  [[ObjectRecord]]: {
    Type: ObjectEnvironmentRecord,
    [[BindingObject]]: <global object>, // e.g., window in browsers
    Bindings: {
      // Built-in globals (e.g., Math, Array, JSON, etc.)
      greet: { [[Value]]: <greet function object>
    },
    [[OuterEnv]]: null
  },
  [[DeclarativeRecord]]: {
    Type: DeclarativeEnvironmentRecord,
    Bindings: {
      innerFn: { [[Value]]: uninitialized }
    },
    [[OuterEnv]]: null
  },
  [[GlobalThisValue]]: <global object>,
  [[OuterEnv]]: null
}

Code execution evaluates a return statement, producing a ReturnCompletion whose [[Value]] is the greetInner function object.

function greet() {
  const name = 'david'

  return function greetInner() {
    console.log(`hi ${name}`)
  }
}

const innerFn = greet()
innerFn()
ReturnCompletion {
  [[Type]]: RETURN,
  [[Value]]: <greetInner function object>
}

The greet execution context is then removed from the execution context stack. The innerFn binding is initialized with that [[Value]].

Even though the greet execution context has been removed from the execution context stack, the environment record created for greet is still reachable. The greetInner function object's [[Environment]] property references that environment record, preventing it from being garbage collected.

function greet() {
  const name = 'david'

  return function greetInner() {
    console.log(`hi ${name}`)
  }
}

const innerFn = greet()
innerFn()
GlobalExecutionContext = {
  LexicalEnvironment: GlobalEnvironmentRecord,
  VariableEnvironment: GlobalEnvironmentRecord
}

<greet function object> = {
  [[Call]]: <internal method>,
  [[Environment]]: GlobalEnvironmentRecord,
  [[ThisMode]]: GLOBAL
}

<greetInner function object> = {
  [[Call]]: <internal method>,
  [[Environment]]: GreetEnvironmentRecord,
  [[ThisMode]]: GLOBAL
}

GreetEnvironmentRecord = {
  type: FunctionEnvironmentRecord
  Bindings: {
    name: { [[Value]]: 'david' },
    greetInner: { [[Value]]: uninitialized }
  },
  [[ThisValue]]: GlobalEnvironmentRecord.[[GlobalThisValue]],
  [[ThisBindingStatus]]: INITIALIZED,
  [[FunctionObject]]: <greet function object>,
  [[NewTarget]]: undefined,
  [[OuterEnv]]: <greet function object>.[[Environment]]
}

GlobalEnvironmentRecord = {
  Type: GlobalEnvironmentRecord,
  [[ObjectRecord]]: {
    Type: ObjectEnvironmentRecord,
    [[BindingObject]]: <global object>, // e.g., window in browsers
    Bindings: {
      // Built-in globals (e.g., Math, Array, JSON, etc.)
      greet: { [[Value]]: <greet function object>
    },
    [[OuterEnv]]: null
  },
  [[DeclarativeRecord]]: {
    Type: DeclarativeEnvironmentRecord,
    Bindings: {
      innerFn: { [[Value]]: <greetInner function object>}
    },
    [[OuterEnv]]: null
  },
  [[GlobalThisValue]]: <global object>,
  [[OuterEnv]]: null
}

Code evaluation reaches another call expression innerFn(). The innerFn binding holds the greetInner function object as its value. greetInner internal [[Call]] method is invoked and a new execution context is created along with a new function environment record.

function greet() {
  const name = 'david'

  return function greetInner() {
    console.log(`hi ${name}`)
  }
}

const innerFn = greet()
innerFn()
GreetInnerExecutionContext = {
  LexicalEnvironment: GreetInnerEnvironmentRecord,
  VariableEnvironment: GreetInnerEnvironmentRecord
}

GreetInnerEnvironmentRecord = {
  type: FunctionEnvironmentRecord
  Bindings: {},
  [[ThisValue]]: GlobalEnvironmentRecord.[[GlobalThisValue]],
  [[ThisBindingStatus]]: INITIALIZED,
  [[FunctionObject]]: <greet function object>,
  [[NewTarget]]: undefined,
  [[OuterEnv]]: <greetInner function object>.[[Environment]]
}

GlobalExecutionContext = {
  LexicalEnvironment: GlobalEnvironmentRecord,
  VariableEnvironment: GlobalEnvironmentRecord
}

<greetInner function object> = {
  [[Call]]: <internal method>,
  [[Environment]]: GreetEnvironmentRecord,
  [[ThisMode]]: GLOBAL
}

GreetEnvironmentRecord = {
  type: FunctionEnvironmentRecord
  Bindings: {
    name: { [[Value]]: 'david' },
    greetInner: { [[Value]]: uninitialized }
  },
  [[ThisValue]]: GlobalEnvironmentRecord.[[GlobalThisValue]],
  [[ThisBindingStatus]]: INITIALIZED,
  [[FunctionObject]]: <greet function object>,
  [[NewTarget]]: undefined,
  [[OuterEnv]]: <greet function object>.[[Environment]]
}

GlobalEnvironmentRecord = {
  Type: GlobalEnvironmentRecord,
  [[ObjectRecord]]: {
    Type: ObjectEnvironmentRecord,
    [[BindingObject]]: <global object>, // e.g., window in browsers
    Bindings: {
      // Built-in globals (e.g., Math, Array, JSON, etc.)
      greet: { [[Value]]: <greet function object>
    },
    [[OuterEnv]]: null
  },
  [[DeclarativeRecord]]: {
    Type: DeclarativeEnvironmentRecord,
    Bindings: {
      innerFn: { [[Value]]: <greetInner function object>}
    },
    [[OuterEnv]]: null
  },
  [[GlobalThisValue]]: <global object>,
  [[OuterEnv]]: null
}

Code evaluation reaches the console.log and hi david is logged to the console. The value undefined is implicitly returned and the execution context is removed from the execution context stack.

function greet() {
  const name = 'david'

  return function greetInner() {
    console.log(`hi ${name}`)
  }
}

const innerFn = greet()
innerFn()

Each Closure Has Its Own Environment Record

The closure here is the arrow function () => setCounter(counter + 1) passed to addEventListener and the setupCounter environment record. This example is a bit more difficult to see how the closure is formed since setupCounter doesn't return anything. The arrow function object has a reference to the setupCounter environment record through its [[Environment]] internal slot.

The arrow function object is registered as an event listener on the element, so it remains reachable and will not be garbage collected as long as the element remains in the DOM.

JavaScript
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'))

// <button id="counter"></button>
// <button id="counter2"></button>

At this point, the compilation phase has finished and executable code has been generated. The JavaScript engine will create an execution context for this block of code.

The JavaScript engine creates a new execution context along with a new environment record and pushes it onto the execution context stack. It then performs GlobalDeclarationInstantiation, which creates and initializes bindings in that environment record. Function declarations are initialized to function objects, var bindings are initialized to undefined, and let and const bindings are created but remain uninitialized until their declarations are evaluated.

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'))
GlobalExecutionContext = {
  LexicalEnvironment: GlobalEnvironmentRecord,
  VariableEnvironment: GlobalEnvironmentRecord
}

<setupCounter function object> = {
  [[Call]]: <internal method>,
  [[Environment]]: GlobalEnvironmentRecord,
  [[ThisMode]]: GLOBAL
}

GlobalEnvironmentRecord = {
  Type: GlobalEnvironmentRecord,
  [[ObjectRecord]]: {
    Type: ObjectEnvironmentRecord,
    [[BindingObject]]: <global object>, // e.g., window in browsers
    Bindings: {
      // Built-in globals (e.g., Math, Array, JSON, etc.)
      setupCounter: { [[Value]]: <setupCounter function object>
    },
    [[OuterEnv]]: null
  },
  [[DeclarativeRecord]]: {
    Type: DeclarativeEnvironmentRecord,
    Bindings: {},
    [[OuterEnv]]: null
  },
  [[GlobalThisValue]]: <global object>,
  [[OuterEnv]]: null
}

When evaluation reaches the call expression, the setupCounter function object’s internal [[Call]] method is invoked. A new function execution context is created with a new function environment record and pushed onto the execution context stack. FunctionDeclarationInstantiation is then performed, creating and initializing bindings in the function environment record.

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'))
SetupCounterExecutionContext = {
  LexicalEnvironment: SetupCounterEnvironmentRecord,
  VariableEnvironment: SetupCounterEnvironmentRecord
}

SetupCounterEnvironmentRecord = {
  type: FunctionEnvironmentRecord,
  Bindings: {
    element: { [[Value]]: <#counter element> },
    counter: { [[Value]]: uninitialized },
    setCounter: { [[Value]]: uninitialized}
  },
  [[ThisBindingStatus]]: INITIALIZED,
  [[ThisValue]]: GlobalEnvironmentRecord.[[GlobalThisValue]],
  [[FunctionObject]]: <setupCounter function object>,
  [[OuterEnv]]: <setupCounter function object>.[[Environment]]
}


GlobalExecutionContext = {
  LexicalEnvironment: GlobalEnvironmentRecord,
  VariableEnvironment: GlobalEnvironmentRecord
}

<setupCounter function object> = {
  [[Call]]: <internal method>,
  [[Environment]]: GlobalEnvironmentRecord,
  [[ThisMode]]: GLOBAL
}

GlobalEnvironmentRecord = {
  Type: GlobalEnvironmentRecord,
  [[ObjectRecord]]: {
    Type: ObjectEnvironmentRecord,
    [[BindingObject]]: <global object>, // e.g., window in browsers
    Bindings: {
      // Built-in globals (e.g., Math, Array, JSON, etc.)
      setupCounter: { [[Value]]: <setupCounter function object>
    },
    [[OuterEnv]]: null
  },
  [[DeclarativeRecord]]: {
    Type: DeclarativeEnvironmentRecord,
    Bindings: {},
    [[OuterEnv]]: null
  },
  [[GlobalThisValue]]: <global object>,
  [[OuterEnv]]: null
}

After the bindings have been initialized from FunctionDeclarationInstantiation, the engine evaluates the statement list associated with that function execution context.

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'))
SetupCounterExecutionContext = {
  LexicalEnvironment: SetupCounterEnvironmentRecord,
  VariableEnvironment: SetupCounterEnvironmentRecord
}

SetupCounterEnvironmentRecord = {
  type: FunctionEnvironmentRecord,
  Bindings: {
    element: { [[Value]]: <#counter element> },
    counter: { [[Value]]: 0 },
    setCounter: { [[Value]]: uninitialized}
  },
  [[ThisBindingStatus]]: INITIALIZED,
  [[ThisValue]]: GlobalEnvironmentRecord.[[GlobalThisValue]],
  [[FunctionObject]]: <setupCounter function object>,
  [[OuterEnv]]: <setupCounter function object>.[[Environment]]
}


GlobalExecutionContext = {
  LexicalEnvironment: GlobalEnvironmentRecord,
  VariableEnvironment: GlobalEnvironmentRecord
}

<setupCounter function object> = {
  [[Call]]: <internal method>,
  [[Environment]]: GlobalEnvironmentRecord,
  [[ThisMode]]: GLOBAL
}

GlobalEnvironmentRecord = {
  Type: GlobalEnvironmentRecord,
  [[ObjectRecord]]: {
    Type: ObjectEnvironmentRecord,
    [[BindingObject]]: <global object>, // e.g., window in browsers
    Bindings: {
      // Built-in globals (e.g., Math, Array, JSON, etc.)
      setupCounter: { [[Value]]: <setupCounter function object>
    },
    [[OuterEnv]]: null
  },
  [[DeclarativeRecord]]: {
    Type: DeclarativeEnvironmentRecord,
    Bindings: {},
    [[OuterEnv]]: null
  },
  [[GlobalThisValue]]: <global object>,
  [[OuterEnv]]: null
}

The arrow function is evaluated and InstantiateArrowFunctionExpression is performed. The new function object's [[Environment]] internal slot is set to the setupCounter environment record, and the setCounter binding is initialized with that function object.

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'))
SetupCounterExecutionContext = {
  LexicalEnvironment: SetupCounterEnvironmentRecord,
  VariableEnvironment: SetupCounterEnvironmentRecord
}

SetupCounterEnvironmentRecord = {
  type: FunctionEnvironmentRecord,
  Bindings: {
    element: { [[Value]]: <#counter element> },
    counter: { [[Value]]: 0 },
    setCounter: { [[Value]]: <setCounter function object> }
  },
  [[ThisBindingStatus]]: INITIALIZED,
  [[ThisValue]]: GlobalEnvironmentRecord.[[GlobalThisValue]],
  [[FunctionObject]]: <setupCounter function object>,
  [[OuterEnv]]: <setupCounter function object>.[[Environment]]
}

<setCounter function object> = {
  [[Call]]: <internal method>,
  [[Environment]]: SetupCounterEnvironmentRecord,
  [[ThisMode]]: LEXICAL
}


GlobalExecutionContext = {
  LexicalEnvironment: GlobalEnvironmentRecord,
  VariableEnvironment: GlobalEnvironmentRecord
}

<setupCounter function object> = {
  [[Call]]: <internal method>,
  [[Environment]]: GlobalEnvironmentRecord,
  [[ThisMode]]: GLOBAL
}

GlobalEnvironmentRecord = {
  Type: GlobalEnvironmentRecord,
  [[ObjectRecord]]: {
    Type: ObjectEnvironmentRecord,
    [[BindingObject]]: <global object>, // e.g., window in browsers
    Bindings: {
      // Built-in globals (e.g., Math, Array, JSON, etc.)
      setupCounter: { [[Value]]: <setupCounter function object>
    },
    [[OuterEnv]]: null
  },
  [[DeclarativeRecord]]: {
    Type: DeclarativeEnvironmentRecord,
    Bindings: {},
    [[OuterEnv]]: null
  },
  [[GlobalThisValue]]: <global object>,
  [[OuterEnv]]: null
}

The second argument passed to addEventListener is an arrow function. That arrow function is evaluated before addEventListener is called, producing a new function object whose [[Environment]] internal slot is set to the setupCounter environment record. That function object is then passed to addEventListener, which registers it as the event listener on element.

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'))
SetupCounterExecutionContext = {
  LexicalEnvironment: SetupCounterEnvironmentRecord,
  VariableEnvironment: SetupCounterEnvironmentRecord
}

SetupCounterEnvironmentRecord = {
  type: FunctionEnvironmentRecord,
  Bindings: {
    element: { [[Value]]: <#counter element> },
    counter: { [[Value]]: 0 },
    setCounter: { [[Value]]: <setCounter function object> }
  },
  [[ThisBindingStatus]]: INITIALIZED,
  [[ThisValue]]: GlobalEnvironmentRecord.[[GlobalThisValue]],
  [[FunctionObject]]: <setupCounter function object>,
  [[OuterEnv]]: <setupCounter function object>.[[Environment]]
}

<click event listener function object> = {
  [[Call]]: <internal method>,
  [[Environment]]: SetupCounterEnvironmentRecord,
  [[ThisMode]]: LEXICAL
}

<setCounter function object> = {
  [[Call]]: <internal method>,
  [[Environment]]: SetupCounterEnvironmentRecord,
  [[ThisMode]]: LEXICAL
}

GlobalExecutionContext = {
  LexicalEnvironment: GlobalEnvironmentRecord,
  VariableEnvironment: GlobalEnvironmentRecord
}

<setupCounter function object> = {
  [[Call]]: <internal method>,
  [[Environment]]: GlobalEnvironmentRecord,
  [[ThisMode]]: GLOBAL
}

GlobalEnvironmentRecord = {
  Type: GlobalEnvironmentRecord,
  [[ObjectRecord]]: {
    Type: ObjectEnvironmentRecord,
    [[BindingObject]]: <global object>, // e.g., window in browsers
    Bindings: {
      // Built-in globals (e.g., Math, Array, JSON, etc.)
      setupCounter: { [[Value]]: <setupCounter function object>
    },
    [[OuterEnv]]: null
  },
  [[DeclarativeRecord]]: {
    Type: DeclarativeEnvironmentRecord,
    Bindings: {},
    [[OuterEnv]]: null
  },
  [[GlobalThisValue]]: <global object>,
  [[OuterEnv]]: null
}

The call expression is evaluated and the setCounter function object’s internal [[Call]] method is invoked. A new function execution context is created with a new function environment record and pushed onto the execution context stack. FunctionDeclarationInstantiation is then performed, instantiating the bindings for the formal parameters and declarations in that environment record. Since the function has no default parameter initializers, these bindings are created in the same environment record.

If the function object's [[ThisMode]] is LEXICAL, the function environment record's [[ThisBindingStatus]] field is set to LEXICAL which indicates this is an ArrowFunction and does not have a local this value.
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'))
SetCounterExecutionContext = {
  LexicalEnvironment: SetCounterEnvironmentRecord,
  VariableEnvironment: SetCounterEnvironmentRecord
}

SetCounterEnvironmentRecord = {
  type: FunctionEnvironmentRecord,
  Bindings: {
    count: { [[Value]]: 0 }
  },
  [[ThisBindingStatus]]: LEXICAL,
  [[FunctionObject]]: <setCounter function object>,
  [[OuterEnv]]: <setCounter function object>.[[Environment]]
}

SetupCounterExecutionContext = {
  LexicalEnvironment: SetupCounterEnvironmentRecord,
  VariableEnvironment: SetupCounterEnvironmentRecord
}

SetupCounterEnvironmentRecord = {
  type: FunctionEnvironmentRecord,
  Bindings: {
    element: { [[Value]]: <#counter element> },
    counter: { [[Value]]: 0 },
    setCounter: { [[Value]]: <setCounter function object> }
  },
  [[ThisBindingStatus]]: INITIALIZED,
  [[ThisValue]]: GlobalEnvironmentRecord.[[GlobalThisValue]],
  [[FunctionObject]]: <setupCounter function object>,
  [[OuterEnv]]: <setupCounter function object>.[[Environment]]
}

<click event listener function object> = {
  [[Call]]: <internal method>,
  [[Environment]]: SetupCounterEnvironmentRecord,
  [[ThisMode]]: LEXICAL
}

<setCounter function object> = {
  [[Call]]: <internal method>,
  [[Environment]]: SetupCounterEnvironmentRecord,
  [[ThisMode]]: LEXICAL
}

GlobalExecutionContext = {
  LexicalEnvironment: GlobalEnvironmentRecord,
  VariableEnvironment: GlobalEnvironmentRecord
}

<setupCounter function object> = {
  [[Call]]: <internal method>,
  [[Environment]]: GlobalEnvironmentRecord,
  [[ThisMode]]: GLOBAL
}

GlobalEnvironmentRecord = {
  Type: GlobalEnvironmentRecord,
  [[ObjectRecord]]: {
    Type: ObjectEnvironmentRecord,
    [[BindingObject]]: <global object>, // e.g., window in browsers
    Bindings: {
      // Built-in globals (e.g., Math, Array, JSON, etc.)
      setupCounter: { [[Value]]: <setupCounter function object>
    },
    [[OuterEnv]]: null
  },
  [[DeclarativeRecord]]: {
    Type: DeclarativeEnvironmentRecord,
    Bindings: {},
    [[OuterEnv]]: null
  },
  [[GlobalThisValue]]: <global object>,
  [[OuterEnv]]: null
}

Once setCounter finishes execution, its execution context is removed from the execution context stack. Its function environment record is no longer reachable, so it can be garbage collected.

There is also nothing left to evaluate in the setupCounter function body, so its execution context is removed from the execution context stack. Its function environment record remains reachable through the arrow function object registered as the event listener on element, so it cannot be garbage collected.

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'))
SetupCounterEnvironmentRecord = {
  type: FunctionEnvironmentRecord,
  Bindings: {
    element: { [[Value]]: <#counter element> },
    counter: { [[Value]]: 0 },
    setCounter: { [[Value]]: <setCounter function object> }
  },
  [[ThisBindingStatus]]: INITIALIZED,
  [[ThisValue]]: GlobalEnvironmentRecord.[[GlobalThisValue]],
  [[FunctionObject]]: <setupCounter function object>,
  [[OuterEnv]]: <setupCounter function object>.[[Environment]]
}

<click event listener function object> = {
  [[Call]]: <internal method>,
  [[Environment]]: SetupCounterEnvironmentRecord,
  [[ThisMode]]: LEXICAL
}

<setCounter function object> = {
  [[Call]]: <internal method>,
  [[Environment]]: SetupCounterEnvironmentRecord,
  [[ThisMode]]: LEXICAL
}

GlobalExecutionContext = {
  LexicalEnvironment: GlobalEnvironmentRecord,
  VariableEnvironment: GlobalEnvironmentRecord
}

<setupCounter function object> = {
  [[Call]]: <internal method>,
  [[Environment]]: GlobalEnvironmentRecord,
  [[ThisMode]]: GLOBAL
}

GlobalEnvironmentRecord = {
  Type: GlobalEnvironmentRecord,
  [[ObjectRecord]]: {
    Type: ObjectEnvironmentRecord,
    [[BindingObject]]: <global object>, // e.g., window in browsers
    Bindings: {
      // Built-in globals (e.g., Math, Array, JSON, etc.)
      setupCounter: { [[Value]]: <setupCounter function object>
    },
    [[OuterEnv]]: null
  },
  [[DeclarativeRecord]]: {
    Type: DeclarativeEnvironmentRecord,
    Bindings: {},
    [[OuterEnv]]: null
  },
  [[GlobalThisValue]]: <global object>,
  [[OuterEnv]]: null
}

Once the second call expression is evaluated, the same process repeats. The setupCounter function object's internal [[Call]] method is invoked and a new function execution context will be created with a new function environment record and pushed onto the execution context stack.

At this point there are two separate setupCounter function environment records. Each has its own counter binding and remains reachable through the [[Environment]] internal slot of the arrow function object registered as an event listener on the element passed as the argument to setupCounter.

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'))

If the button#counter element is clicked, the arrow function registered as the event listener is invoked. A new execution context is created with a new function environment record and pushed onto the execution context stack. The arrow function object closes over the setupCounter environment record and calls setCounter, updating the counter binding in that environment record.

ClickListenerExecutionContext = {
  LexicalEnvironment: ClickListenerEnvironmentRecord,
  VariableEnvironment: ClickListenerEnvironmentRecord
}

ClickListenerEnvironmentRecord = {
  type: FunctionEnvironmentRecord,
  Bindings: {},
  [[ThisBindingStatus]]: LEXICAL,
  [[FunctionObject]]: <click listener function object>,
  [[OuterEnv]]: <click listener function object>.[[Environment]]
}

SetCounterEnvironmentRecord = {
  type: FunctionEnvironmentRecord,
  Bindings: {
    count: { [[Value]]: 0 }
  },
  [[ThisBindingStatus]]: LEXICAL,
  [[FunctionObject]]: <setCounter function object>,
  [[OuterEnv]]: <setCounter function object>.[[Environment]]
}

SetupCounterExecutionContext = {
  LexicalEnvironment: SetupCounterEnvironmentRecord,
  VariableEnvironment: SetupCounterEnvironmentRecord
}

SetupCounterEnvironmentRecord = {
  type: FunctionEnvironmentRecord,
  Bindings: {
    element: { [[Value]]: <#counter element> },
    counter: { [[Value]]: 0 },
    setCounter: { [[Value]]: <setCounter function object> }
  },
  [[ThisBindingStatus]]: INITIALIZED,
  [[ThisValue]]: GlobalEnvironmentRecord.[[GlobalThisValue]],
  [[FunctionObject]]: <setupCounter function object>,
  [[OuterEnv]]: <setupCounter function object>.[[Environment]]
}

<click listener function object> = {
  [[Call]]: <internal method>,
  [[Environment]]: SetupCounterEnvironmentRecord,
  [[ThisMode]]: LEXICAL
}

<setCounter function object> = {
  [[Call]]: <internal method>,
  [[Environment]]: SetupCounterEnvironmentRecord,
  [[ThisMode]]: LEXICAL
}

GlobalExecutionContext = {
  LexicalEnvironment: GlobalEnvironmentRecord,
  VariableEnvironment: GlobalEnvironmentRecord
}

<setupCounter function object> = {
  [[Call]]: <internal method>,
  [[Environment]]: GlobalEnvironmentRecord,
  [[ThisMode]]: GLOBAL
}

GlobalEnvironmentRecord = {
  Type: GlobalEnvironmentRecord,
  [[ObjectRecord]]: {
    Type: ObjectEnvironmentRecord,
    [[BindingObject]]: <global object>, // e.g., window in browsers
    Bindings: {
      // Built-in globals (e.g., Math, Array, JSON, etc.)
      setupCounter: { [[Value]]: <setupCounter function object>
    },
    [[OuterEnv]]: null
  },
  [[DeclarativeRecord]]: {
    Type: DeclarativeEnvironmentRecord,
    Bindings: {},
    [[OuterEnv]]: null
  },
  [[GlobalThisValue]]: <global object>,
  [[OuterEnv]]: null
}

Closures Close Over a Variable Not its Value

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.

JavaScript
let x = 1
const y = () => x

y() // 1
x++
y() // 2

Closures in Loops

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 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.

JavaScript
for (
  let i = 0, getI = () => i;
  i < 3;
  i++
) {
  console.log(getI());
}

Block Scoped Closures

Closures can also close over block scoped variables.

JavaScript
function foo() {
  const x = 1
  const y = 1
  {
    const y = 2
    return () => console.log(x, y)
  }
}

const bar = foo()
bar() // 1 2

Conclusion

Understanding closures begins with understanding how the JavaScript engine executes code. Once execution contexts and environment records are clear, a closure is simply a function object that retains a reference to the environment record in which it was defined.