Scopes, Closures, Loops in ES5/ES6: An Incomplete Catalog of Approaches

asragab

Ahmad Ragab

Posted on April 19, 2020

Scopes, Closures, Loops in ES5/ES6: An Incomplete Catalog of Approaches

Introduction

The classic problem is that closures (or as I like to think of them "captures") in javascript close over their environment, but that environment is lexically scoped, and not as the braces might easily convince you otherwise, block scoped. Thus, even though var text and var i are declared with the for-loop they are available in the entirety of the function's scope. This also means that their mutations (vars are mutable) are visible to all parts of the function.

Here we are iterating through a loop 10 ten times, and each time we are pushing into the storedClosures array, a function that console logs the value of i and text, later we call the environment, and foreach function in the storedClosures array we call that function.

function environment() {
  var storedClosures = []
  for (var i = 0; i < 10; i++) {
    var text = `text from env: ${i}`
    storedClosures.push(function () {
      // last valid value in the loop is 9, when closure is called i is now 10
      console.log(`${text} | inside closure ${i}`)
    })
  }

  return storedClosures
}

console.log('Broken closure:')
environment().forEach((func) => func())

The surprising result for the uninitiated, is the output looks like this:

Broken closure:
text from env: 9 | inside closure 10
text from env: 9 | inside closure 10
text from env: 9 | inside closure 10
text from env: 9 | inside closure 10
text from env: 9 | inside closure 10
text from env: 9 | inside closure 10
text from env: 9 | inside closure 10
text from env: 9 | inside closure 10
text from env: 9 | inside closure 10
text from env: 9 | inside closure 10

This is because the variable i has the value it has when the scope is finished, which is 10, but the reason the first number is nine and the second is 10, is that the last value i had inside the loop is 9 and only later when the function is called does it close on the value of i after the loop completed. Confusing, right?


We will now review a few common workarounds to this problem, the first three in ES5, and next being the ES6+ solution

Fix 1: In Darkness .bind() Them

// Solution 1 (Pre-ES6): create function to close over outside the environment
function closureFunc(text, i) {
  console.log(`${text} | inside closure ${i}`)
}

function environmentWithBoundClosure() {
  var storedClosures = []
  for (var i = 0; i < 10; i++) {
    var text = `text from env: ${i}`
    // use bind to return new function, with text, i closed over each time during the loop
    storedClosures.push(closureFunc.bind(this, text, i))
  }

  return storedClosures
}

console.log('\nSolution 1 | Using bound closure separately defined (ES5):')
environmentWithBoundClosure().forEach(func => func())

We define a separate function called closureFunc and then inside the loop we call the magical .bind() of which much has been written about, what happens here is a new function is returned by the bind call, with the this variable and arguments modified as necessary. Here, we simply provide the current value of text and i for the new function to close over.

Solution 1 | Using bound closure separately defined (ES5):
text from env: 0 | inside closure 0
text from env: 1 | inside closure 1
text from env: 2 | inside closure 2
text from env: 3 | inside closure 3
text from env: 4 | inside closure 4
text from env: 5 | inside closure 5
text from env: 6 | inside closure 6
text from env: 7 | inside closure 7
text from env: 8 | inside closure 8
text from env: 9 | inside closure 9

Fixed, yeah! Note here as well that the value of i in the text from the "env" as well as inside the closure are aligned, since we don't close over the value of i anymore outside the for-loop itself.

Fix 2: Double your closures, double your funcs

// Solution 2 (Pre-ES6): create doubly nested IIFE and call with i
function environmentWithDoublyNestedClosure() {
  var storedClosures = []
  for (var i = 0; i < 10; i++) {
    var text = `text from env: ${i}`
    storedClosures.push(
      (function (text, i) {
        return function () {
          console.log(`${text} | inside closure ${i}`)
        }
      })(text, i) // IIFE is invoked with the current values of text and i
    )
  }

  return storedClosures
}

console.log('\nSolution 2 | Using nested closure with IIFE (ES5):')
environmentWithDoublyNestedClosure().forEach((func) => func())

This workaround makes use of an IIFE (Immediately Invoked Function Expression), what this does is allows you define a function, and then immediate call it the syntax is a little busy but is something like this:

(function (arg1, arg2) { /*do stuff*/ })(arg1, arg2)

So while we are immediately invoking our function, what we are getting back for our invocation is yet another function. That function or closure has closed over the arguments that were provided during the invocation, the current values of text and i. We get the same fixed results.

Fix 3: forEach FTW

//Solution 3 (Pre-ES6): use forEach to manage iteration
function environmentWithForEach() {
  var storedClosures = []
  var range = Array.apply(null, { length: 10 }).map(Function.call, Number) // ugly range hack
  range.forEach((i) =>
    storedClosures.push(function () {
      var text = `text from env: ${i}`
      console.log(`${text} | inside closure ${i}`)
    })
  )
  return storedClosures
}

console.log('\nSolution 3 | Using ForEach (ES5):')
environmentWithForEach().forEach((func) => func())

You can ignore the ugly range hack, I just wanted some way to generate a list of integers using a range (why this wizardry is required is beyond me). Just imagine you have some other array that you are looping through in order to generate the closures. The real trick is that .forEach() graciously creates a local environment for us to close over every iteration, meaning the i in the range is lexically scoped to the bounds of the forEach call.

Fix 4: let the sunshine in

//Solution 4 (ES 6+): Use let
function environmentWithLet() {
  var storedClosures = []

  //let is required for iteration variable i and the text which creates a block level scope to close over
  for (let i = 0; i < 10; i++) {
    let text = `text from env: ${i}`
    storedClosures.push(function () {
      console.log(`${text} | inside closure ${i}`)
    })
  }

  return storedClosures
}

console.log('\nSolution 4 | Using Let (ES6+):')
environmentWithLet().forEach((func) => func())

Simply changing the vars to lets for the i and text variables changes the scope of the variable to be at the block level, thus they are closed over each time through the iteration - providing the proper results again.

💖 💪 🙅 🚩
asragab
Ahmad Ragab

Posted on April 19, 2020

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related