Understanding JavaScript Closures in For Loops

JavaScript

How do you pass a variable into a function inside a loop? It always ends up taking the last value! What gives?

Let's suppose you're lucky enough to work for a company that gives annual cost-of-living pay increases. Let's also suppose the corporate payroll system is written in JavaScript. (Why not? everything seems to be JavaScript these days)

And, because we're big enterprise, our system is rife with inefficiency and we've over-engineered everything. Below is what the payroll function looks like. (This is a totally contrived example... so you'll have to practice suspension of disbelief rather than ask why it's designed so poorly)

// Queue jobs to assign employee pay increasesfunction queueEmployeeRaises(employees, jobQueue, database, multiplier) {  for (var i = 0; i < employees.length; i++) {    var employee = employees[i];    jobQueue.push(function() {      if (employee.title == 'CEO') {        employee.salary = employee.salary * Math.max(1.20, multiplier);      }      else {        employee.salary = employee.salary * Math.min(multiplier, 1.02);      }      database.save(employee);    });  }}

What is wrong with this code?

Aside from the horrendous architecture (e.g. violation of SRP)

Aside from the socio-economic disparity between America's working classes...

We are pushing "jobs" onto a queue -- which by itself is fine. Part of the problem is that our "job" function takes variables from outside and uses them inside.

This is called a closure.

It is said that the job function closes over, or has captured, employee -- which just means that we've saved a reference to the variable.

Closures are also fine, when used properly. But the other part of the problem is that we've got a loop that is changing the variable on which the job function has closed over.

Think of our reference to employee as we're holding onto a box. When we go to use the employee value, what we're really doing is opening the box and taking out what's inside... and the for-loop is changing what's inside the box.

Stepping through the problem

Suppose we've got the following employees:

var employees = [  {    name: 'Alice',    title: 'Jr. Software Developer',    salary: 65000,  },  {    name: 'Bob',    title: 'Sr. Software Developer',    salary: 90000,  },  {    name: 'Charlie',    title: 'CEO',    salary: 5000000,  },];

Okay, so we've got two software developers and a CEO.

Now imagine you're single-stepping through the function with our employee array. This is what memory looks like as the code executes: Understanding JavaScript Closures: Single stepping through our function

OMG! I think we've just discovered how CEO salaries have become so disproportionately high!

Here is a rough pseudocode account of what happens as the code executes...


    call queueEmployeeRaises
      allocate memory for i
      allocate memory for employee
      set i to 0
      compare i < employees.length // 0 < 3
      set employee to Alice
      create closure #1
      call jobQueue.push
      set i++
      compare i < employees.length // 1 < 3
      set employee to Bob
      create closure #2
      call jobQueue.push
      set i++
      compare i < employees.length // 2 < 3
      set employee to Charlie
      create closure #3
      call jobQueue.push
      set i++
      compare i < employees.length // 3 == 3
      break out of loop
      memory for i deallocated
      memory for employee persists because it's been captured in closures
    end queueEmployeeRaises
    
    ... // some time later the 'jobs' are executed
    
    call closure #1
      lookup employee in memory // Charlie
      compare employee.title == 'CEO' // true
      set employee.salary = employee.salary * 1.20 // $5MM + 20% = $6MM
      call database.save
    end closure #1
    call closure #2
      lookup employee in memory // Charlie
      compare employee.title == 'CEO' // true
      set employee.salary = employee.salary * 1.20 // $6MM + 20% = $7.2MM
      call database.save
    end closure #2
    call closure #3
      lookup employee in memory // Charlie
      compare employee.title == 'CEO' // true
      set employee.salary = employee.salary * 1.20 // $7.2MM + 20% = $8.64MM
      call database.save
    end closure #3
    

Yeah, well what if...

Maybe now you're thinking that you can just move var employee = employees[i] inside the closure...

function queueEmployeeRaises(employees, jobQueue, database, multiplier) {  for (var i = 0; i < employees.length; i++) {    // var employee = employees[i];    jobQueue.push(function() {      var employee = employees[i]; // How about this?

Nope, still wrong.

We've only moved the problem from employee to i. The closure is now closing over i and the loop is still changing the value.

This is actually worse because i will hold the value 3 when our jobs run and, because arrays are 0-indexed, employees[3] === undefined so we'll get an error "can't read property title from undefined".

Raises for nobody! Sorry, CEO.

ES5 Solution: Change the Closure

Instead of closing over something that changes, we're going to close over something that doesn't change.

function queueEmployeeRaises(employees, jobQueue, database, multiplier) {  for (var i = 0; i < employees.length; i++) {    var e = employees[i]; // changed to e    (function(employee) { // added this. employee is now a parameter      jobQueue.push(function() {        if (employee.title == 'CEO') {          employee.salary = employee.salary * Math.max(1.20, multiplier);        }        else {          employee.salary = employee.salary * Math.min(multiplier, 1.02);        }        database.save(employee);      });     })(e); // added this. Call the function with current employee in e  }}

Wait a minute... Doesn't the parameter still change each time through the loop?

Yes. The key difference here is that the function we added is executing immediately.

i.e. It's running inside the loop as opposed to after the loop has already completed. The closure is now closing over the parameter to our function, which is effectively fixed to the value it was called with rather than the future value of e.

Note: We didn't have to change the variable name from employee to e. I just did that for clarity. We could have left the variable name alone and then there would have been an employee at the queueEmployeeRaises scope and another in the anonymous inner function scope.

ES6 Solution: Buh bye, var

Using the var keyword causes the memory to be allocated once in the scope of queueEmployeeRaises.

We could have solved the problem in ES6 had we used the new let or const keywords, in which case separate memory would have been allocated each time through the loop and there would have been no problem.

// Queue jobs to assign employee pay increasesfunction queueEmployeeRaises(employees, jobQueue, database, multiplier) {  for (var i = 0; i < employees.length; i++) {    const employee = employees[i]; // const instead of var    jobQueue.push(function() {      if (employee.title == 'CEO') {        employee.salary = employee.salary * Math.max(1.20, multiplier);      }      else {        employee.salary = employee.salary * Math.min(multiplier, 1.02);      }      database.save(employee);    });  }}

Further Reading

New to web development? Not sure where to go or what to do next?

Sign up on my email list where I write about modern web development with technology like React, Redux, TypeScript, Webpack and more.