Practical Closure

Practical Closure

In some cases, closure can be tricky to predict. Gathered some of these concepts with code implementations to make the closure easy and interesting.

Definition

A function with a reference from the outer environment creates a closure.

  • JS functions can access its lexical scope (outer scope)
  • Functions can remember their lexical scope no matter where it is executed

Example and Tweaking

function outer () {
  var a = 'hello';
  function inner() {
    console.log(a);
  }
  return inner;
}

outer()(); // prints "hello"

It is not necessary to define variables above the function definition.

function outer() {
  function inner() {
    console.log(a);
  }
  var a = 'hello'; // define variable after function definition
  return inner;
}

outer()(); // prints 'hello'

Instead of using var, we can use let or const instead.

function outer() {
  function inner() {
    console.log(a);
  }
  const a = 'hello';
  return inner;
}

outer()(); // prints 'hello'

We can pass a parameter to the parent method and create a closure.

function outer(b) {
  function inner() {
    console.log(b);
  }
  return inner;
}

outer('I am param')(); // prints "I am param"

Closure can be formed with multiple hierarchies of methods.

function evenOuter() {
  var val = 'From even outer';
  function outer() {
    function inner() {
      console.log(val);
    }
    return inner;
  }
  return outer;
}

evenOuter()()();

Closure took the scoped variable and ignore the upper lexical scope,

var val = 'Global scope';
function outer() {
    function inner() {
      console.log(val);
    }
    var val = 'Outer scope';
    return inner;
  }

outer()(); // prints 'Outer scope'

Advantages of Closures

  • Function currying
  • Optimized higher-order function
  • Memorization
  • Data hiding and encapsulation

Function currying

By function currying, we can create new function for specialized argument and reuse them as needed.

function multiply(a) {
  return function(b) {
    return a * b;
  }
}

// Currying the multiply function
const double = multiply(2);
const triple = multiply(3);

console.log(double(5)); // Output: 10
console.log(triple(5)); // Output: 15

Higher Order Function

Higher order function takes another function as a parameter.

function repeat(fn, n) {
  return function(x) {
    for (let i = 0; i < n; i++) {
      fn(x);
    }
  }
}

function logWithPrefix(prefix) {
  return function(message) {
    console.log(`${prefix}: ${message}`);
  }
}

const repeatLog = repeat(logWithPrefix('INFO'), 3);
repeatLog('Hello'); // Output: INFO: Hello (three times)

Memorization

Similar to Function currying example.

Data Hiding and Encapsulation

Consider the following code,

let count = 0;
function increment() {
  count += 1
  console.log(count);
}

increment();
count = 2;

Here the major flow is, we can increment the count directly without using the increment method.

To ensure, always use the increment to increment the count, we can make use of closure.

function counter() {
  let count = 0;
  function increment() {
    count += 1
    console.log(count);
  }
  return increment;
}

const myIncrement = counter();
myIncrement(); // print 1
myIncrement(); // print 2

An interesting fact is, every time we call the counter method, it always creates a new session.

function counter() {
  let count = 0;
  function increment() {
    count += 1
    console.log(count);
  }
  return increment;
}

const myIncrement1 = counter();
myIncrement1(); // prints 1
myIncrement1(); // prints 2

const myIncrement2 = counter();
myIncrement2(); // prints 1
myIncrement2(); // prints 2

If we need another function, decrement then the code snippet is not scalable. In this case, we can make use of Function Constructor.

function Counter() {
  let count = 0;
  this.increment = function() {
    count += 1
    console.log(count);
  }
  this.decrement = function () {
    count -= 1;
    console.log(count)
  }
}

const myCounter = new Counter();
myCounter.increment(); // print 1
myCounter.increment(); // print 2
myCounter.decrement(); // print 1

Disadvantages

If not handled properly,

  • Can cause overconsumption of memory
  • Memory leaks