Back to Home

JS Closures: Lexical Environment and Examples

Closures in JavaScript provide access for the inner function to outer variables after the outer one completes. The material covers theory from lambda calculus, practices in loops, OOP emulation, and engine optimization. Code examples for middle/senior developers.

How Closures Work in JS: Code and Loop Pitfalls
Advertisement 728x90

JavaScript Closures: Lexical Environment Mechanism and Practical Applications

A closure in JavaScript is a combination of a function and its lexical environment, where an inner function retains access to variables from an outer scope even after the outer function has completed execution. This enables state encapsulation and predictable behavior in asynchronous contexts.

The concept originated in Alonzo Church's lambda calculus. Higher-order functions return or accept other functions, capturing free variables—those used inside but declared outside.

A basic counter example demonstrates the principle:

Google AdInline article slot
function counter() {
  let count = 0
  return function(step) {
    count = count + step
    return count
  }
}

const myCounter = counter()
console.log(myCounter(1)) // 1
console.log(myCounter(1)) // 2
console.log(myCounter(1)) // 3

The inner function closes over count, preventing garbage collection. This is a live reference: changes are reflected in real time.

Closures as an OOP Analogue

Closures emulate private fields and public APIs of objects. A counter with methods:

function Counter(initialValue = 0) {
  let count = initialValue
  return {
    increment: function(step = 1) {
      return count += step
    },
    decrement: function(step = 1) {
      return count -= step
    },
    getValue: function() {
      return count
    },
    reset: function() {
      return count = initialValue
    }
  }
}

const myCounter = Counter(10)
console.log(myCounter.getValue()) // 10
console.log(myCounter.increment(5)) // 15

An equivalent using classes with private fields (#count) shows identical logic. In Pinia/Vuex stores, closures encapsulate reactive state:

Google AdInline article slot
export const useCounterStore = defineStore('counter', () => {
  const count = ref(0)
  const initValue = 0
  function increment(step) {
    count.value += step
  }
  return { count, increment }
})

Nuances in Loops and Iterators

Closure behavior in loops depends on hoisting and block scope.

With var—a shared variable for all iterations:

for (var i = 0; i < 3; i++) {
  setTimeout(function() {
    console.log(i) // 3, 3, 3
  }, 100)
}

With let—a separate lexical environment per iteration:

Google AdInline article slot
for (let i = 0; i < 3; i++) {
  setTimeout(function() {
    console.log(i) // 0, 1, 2
  }, 100)
}

Iterators like forEach, map, filter create closures automatically:

[0, 1, 2].forEach(function(i) {
  setTimeout(function() {
    console.log(i) // 0, 1, 2
  }, 100)
})

Practice: Callbacks and Events

In asynchronous code, closures solve context issues.

Ajax with a unique ID:

function ajaxRequest(url) {
  let requestId = Math.random().toString(36).substring(2)
  fetch(url).then(data => {
    console.log(`Request ${requestId} completed:`, data)
  }).catch(error => {
    console.log(`Request ${requestId} failed:`, error.message)
  })  
}

Event handlers:

function clickSetup(elemId, message) {
  const elem = document.getElementById(elemId)
  elem.addEventListener('click', function() {
    console.log(message)
  })
}

Arrow functions work identically to regular ones.

Optimization and Debugging

JS engines (V8) exclude unused variables from closures to save memory. In DevTools (Scope → Closure), only relevant variables are visible.

Key practices:

  • Use let/const in loops for block scope.
  • Prefer closures for private state without classes.
  • In asynchronous callbacks, fix context early.
  • Monitor memory in long-lived closures.

Key Takeaways

  • Closure = function + lexical environment with live references to variables.
  • let in loops creates per-iteration closures, var does not.
  • Emulates OOP private fields, used in Vuex/Pinia stores.
  • Optimized by engines: unused vars are excluded.
  • Essential for asynchronous callbacks and events.

— Editorial Team

Advertisement 728x90

Read Next