Closures

Closures in Javascript

Javascript allows us to pass functions around as data much like any other data type, and the javascript engine provides us with lexical scope. Due to these two characteristics of the language we have Closures. A Closure is a combination of the function and the lexical environment in which it was declared, so in this way, Closures let us access variables from an enclosing scope even after it leaves the scope in which it was declared. This might sound confusing but after going through some examples this begins to make more sense.

For example:


// Nested functions that can only be called from the function a()
function a() {
  let grandpa = 'grandpa'
  return function b() {
    let father = 'father'
    return function c () {
      let son = 'son'
      return `${grandpa} > ${father} > ${son}`
    }
  }
}

// if we call this function, we will expect it to just return function b
a();

// if we call this function 2 steps deeper, it will return the result from function c
a()()();

// returns 'granpa > father > son'

With Javascript, any data including functions that are written with a closure don't get garbage collected from the heap when the functions are popped off our call stack if they are referenced by a child function. Our data, in this case grandpa and father, are being referenced by function c(), so they are put in a special place in the Javascript engines memory heap and they are not cleaned up by the garbage collector. When the function c() is run, it will look at its variable environment to see if any referenced variables exist. If they don't, it will check the special place in our memory heap reserved for closures to find them.

This also works with the arguments from a parent function.

Here's an example using arrow functions in modern javascript:

const closure = (string) => (name) => (name2) =>
console.log(`${string} ${name} ${name2}`)

// if we now run the function, we'll see that we can access all arguments through this

closure('hello')('john')('jones'); // returns 'hello john jones'

This has a few major benefits. The first major benefit we will cover is memory efficiency.

// if we have a calculation heavy or otherwise intensive function and wanted to access it multiple times, we could do this
function example1(idx) {
  const bigArray = new Array(7000).fill('🦆')
  console.log('created')
  return bigArray[idx]
}

example1(812)
example1(961)
example1(128)

// if we did this however, we would soon find that each time we call the function, our array is being created each time as shown in our console log.


// Instead of doing it that way, we could do it like this instead so the intensive function is only called once, and the resulting data that is required is cached.

function example2(idx) {
  const bigArray = new Array(7000).fill('🦆')
  console.log('created')
  return function(idx) {
    return bigArray[idx]
  }
}

const getExample = example2();

getExample(812)
getExample(961)
getExample(128)

// here we will see that in this example, our data is only created when it is first called. Every time after that, it is referenced directly. This results in more efficient code.

The other benefit is that it allows for is encapsulation.

Here's a silly demonstration:


const spawnSquidWizard = () => {
  let timeWithoutSquids = 0;
  const passTime = () => timeWithoutSquids++;
  const timeSinceLastSquid = () => timeWithoutSquids;
  const summonSquid = () => {
    timeWithoutSquids = -1;
    return '🦑';
  }

  setInterval(passTime, 1000);
  return {
    summonSquid: summonSquid,
    timeSinceLastSquid: timeSinceLastSquid
  }
}

const OhNo = spawnSquidWizard();

OhNo.timeSinceLastSquid();

// here if we run this, we'll find that at any time we can see how long it has been since summonSquid() was last called.

If we were to change this code a little, we can also only allow access to certain data or functions from the squid wizard, so it's not possible for everyone to summon a squid. This is one of the major benefits with Closures from encapsulation, and allows for us to create more secure code.


const spawnSquidWizard = () => {
  let timeWithoutSquids = 0;
  const passTime = () => timeWithoutSquids++;
  const timeSinceLastSquid = () => timeWithoutSquids;
  const summonSquid = () => {
    timeWithoutSquids = -1;
    return '🦑';
  }

  setInterval(passTime, 1000);
  return {
    //summonSquid: summonSquid,
    // now that summonSquid is commented out, it cannot be accessed from outside, only the time since the last squid summon can be called
    timeSinceLastSquid: timeSinceLastSquid
  }
}

const OhNo = spawnSquidWizard();

OhNo.timeSinceLastSquid();


Here's a more practical way we can use closures. Suppose we have a function that we want to initialize only once, and any further calls not to reset the data in that function. How would we do that?

With closures, it can be done very simply:

let friend;
function init() {
  let called = 0;
  
  return function() {
    if (called > 0) {
      return;
    } else {
      friend = '🐢';
      called++;

      console.log('friend has been chosen!')
    }
  }
}

const initialize = init();

initialize();

// in this example, the code will only be initialized once regardless of how many times we call initialize(). This solution is a little naive as init() can still be called. To solve this, we could go a step further:


// return initialize only after we've run through the init.

let friend;
function init() {
  let called = 0;
  
  return function() {
    if (called > 0) {
      return;
    } else {
      friend = '🐢';
      called++;

      console.log('friend has been chosen!')
    }
  }
}

const initialize = init();

initialize();



Here's another problem:


// here we want to be able to log "I am at index 'x' each time we cycle through our array" 
const array = [1,2,3,4];
for (var i=0; i < array.length; i++) {
  setTimeout(function() {
    console.log('I am at index ' + i)
  }, 1000)
}

// In this first case, it will only print "I am at index 4".

// By changing the for loop from using var to let, it will give us a proper solution. This is the easiest way. This is because let allows us to use block scoping, and thus each index is scoped when it is run.

const array = [1,2,3,4];
for (let i=0; i < array.length; i++) {
  setTimeout(function() {
    console.log('I am at index ' + i)
  }, 1000)
}

// If for some reason we couldn't use the let keyword, we could instead use closures to solve this problem. By wrapping out setTimeout() with a function, our reference to i is preserved with the closure. We can then call this with a function to get the correct value.

const array = [1,2,3,4];
for (var i=0; i < array.length; i++) {
  (function(closureI) {
    setTimeout(function() {
        console.log('I am at index ' + closureI)
      }, 1000)
  })(i)
}

This sums up a simple look at closures in Javascript and a few ways they can be used. This is a very powerful concept and is used all throughout modern web development including in frameworks like React, Nextjs, Vue, and so on.