Understanding JavaScript closure and where it can be used

  sonic0002        2023-03-05 02:17:08       1,571        0    

Closure

The official definition of closure is: an expression (usually a function) that has many variables and is bound to an environment that includes those variables.

In JavaScript, closure refers to the ability of a function to access the lexical scope in which it was defined, even after the function has been executed and left that scope. This ability is due to the fact that when a function is created, it generates a closure that includes a reference to the definition environment of the current function, allowing the function to continue to access those variables within that environment.

Below is an example of closure:

function makeCounter() {
  var count = 0;
  return function() {
    return ++count;
  };
}

var counter = makeCounter();

console.log(counter()); // Output: 1
console.log(counter()); // Output: 2
console.log(counter()); // Output: 3

In this example, the makeCounter function returns an anonymous function and defines a count variable within the scope of the makeCounter function. When the makeCounter function is executed, it returns the anonymous function and assigns it to the variable counter. Each time the counter function is called, it accesses the count variable within the scope of the makeCounter function and increments it. Since the anonymous function creates a closure when it is created, the counter function can still access the count variable within the scope of the makeCounter function even after the makeCounter function has finished executing, thereby implementing the functionality of a counter.

Closures have the following characteristics:

  • Closures can access variables in the outer function's scope, even after the outer function has returned.
  • Closures hold references to the outer function's scope, which can lead to memory leaks.
  • Closures can share state between multiple functions, but care must be taken to avoid accidentally modifying that state.

Due to the special properties of closures, they are widely used in JavaScript to implement modularity, encapsulate private variables, and so on. However, since closures can lead to problems such as memory leaks, they should be used with care.

How closure is implemented

The implementation principle of closures is based on JavaScript's function scope and scope chain mechanism. When a function is defined, it creates a new scope and saves the current variable environment in that scope. When the function is executed, it creates a new execution environment and saves the current scope chain in that execution environment. After the function is executed, it destroys the execution environment and scope chain together, but the variables in the scope are still saved in memory.

When a function returns an inner function, the inner function can still access the outer function's scope and variables because its scope chain includes the outer function's scope chain. This creates a closure where the inner function can access the outer function's variables, and these variables are not destroyed until the inner function is destroyed.

Below is an example demonstrating how closure is implemented:

function outer() {
  let x = 10;
  return function inner() {
    console.log(x);
  };
}

const innerFn = outer();
innerFn(); // Output 10

In this example, the outer function returns an inner function that can access the variable x in the outer function. After the outer function is executed, the variable x is still saved in memory because the inner function forms a closure and can access the variables and scope of the outer function.

Usages of closures

Implementing private variables, methods and modularization

// Modularized counter
const counterModule = (function() {
  let count = 0; // private variable

  function increment() { // private method
    count++;
    console.log(`counter value: ${count}`);
  }

  function reset() { // private method
    count = 0;
    console.log('counter is reset');
  }

  return { // expose public method
    increment,
    reset
  }
})();

// use module
counterModule.increment(); // counter value: 1
counterModule.increment(); // counter value: 2
counterModule.reset(); // counter is reset

In the above code, we use an immediately invoked function expression (IIFE) that returns an object containing two public methods, increment() and reset(), which can be accessed from the outside. The count variable and the increment() and reset() methods are private. This way, we can organize our code in a modular way, avoiding global variable pollution, while also protecting private variables and methods from external interference.

Implementing function memoization

function memoize(fn) {
  const cache = {}; // cache counter result

  return function(...args) {
    const key = JSON.stringify(args); // convert the arguments as cache key

    if (cache[key] === undefined) { // if no result, hen caulcate and cache
      cache[key] = fn.apply(this, args);
    }

    return cache[key]; // return cached calculation result
  };
}

function factorial(n) {
  console.log(`calculating ${n} factorial`);
  if (n === 0) {
    return 1;
  }
  return n * factorial(n - 1);
}

const memoizedFactorial = memoize(factorial);

console.log(memoizedFactorial(5)); // calculating 5 factorial 120
console.log(memoizedFactorial(5)); // 120 
console.log(memoizedFactorial(3)); // calculating 3 factorial 6
console.log(memoizedFactorial(3)); // 6

In the above code, we defined a memoize() function that takes a function fn as a parameter and returns a new function that caches the result of fn's calculation to avoid repeated calculations. Specifically, we used an object called "cache to store the calculation result, and returned a closure that references the cache object and fn function in the outer memoize() function. In the closure, we used JSON.stringify() to convert the input parameters into a string as the cache key, then checked if the result was already in the cache. If so, we returned it directly; otherwise, we calculated the result and saved it to the cache. Finally, we can use the memoize() function to wrap any function that needs to be memoized to avoid repeated calculations. In the above code, we wrapped a function called factorial() that calculates factorials using memoize(), and named it "memoizedFactorial". We can see that the first time we calculate the factorial of a number, it outputs a message that it's currently calculating it. However, when we calculate the same number again, it doesn't output the message because the result is already cached.

Avoid scope issues in loops

This technique can also be used to avoid scope issues in loops. When we define a function inside a loop, it may reference the loop variable, but because of the way JavaScript's scoping works, the value of the loop variable may not be what we expect when the function is called. By using a closure to create a new scope for each iteration of the loop, we can avoid this issue.

function createFunctions() {
  const result = [];

  for (var i = 0; i < 5; i++) {
    result[i] = function(num) {
      return function() {
        return num;
      };
    }(i);
  }

  return result;
}

const funcs = createFunctions();
console.log(funcs[0]()); // 0
console.log(funcs[1]()); // 1
console.log(funcs[2]()); // 2
console.log(funcs[3]()); // 3
console.log(funcs[4]()); // 4
In the above code, we define a function createFunctions() that returns an array containing five functions. The purpose of these functions is to return their index in the array. We use a closure to avoid the scope problem in the loop. Specifically, we define an immediately invoked anonymous function in the loop that takes a parameter num and returns a new function that always returns num. Then, we immediately call this anonymous function, passing in i as the argument, and save the returned function to the corresponding position in the array result. Since the anonymous function returns a new function and this new function refers to the variable num in the outer function createFunctions(), each function will record its index in the array. Therefore, when we call these functions, they will return their index in the array, rather than the value of the loop variable i. Finally, we use the createFunctions() function to create an array of five functions that return their own index, and call these functions separately, outputting their return values.

Save state in asynchronous programming

function createIncrementer() {
  let count = 0;

  function increment() {
    count++;
    console.log(`Count: ${count}`);
  }

  return {
    incrementAsync() {
      setTimeout(() => {
        increment();
      }, 1000);
    }
  };
}

const incrementer = createIncrementer();

incrementer.incrementAsync(); // Count: 1
incrementer.incrementAsync(); // Count: 2
incrementer.incrementAsync(); // Count: 3

In the code above, we defined a function createIncrementer() that returns an object containing a method incrementAsync(). This method will call an internal increment() function after 1 second. The increment() function accesses the variable count defined in the outer function createIncrementer() through closure, so it can continue to keep track of the count between multiple calls to the incrementAsync() method. We created an incrementer object and called its incrementAsync() method multiple times, and each time it will output the current count value after 1 second. Note that we did not explicitly pass any parameters in this process, but instead used closure to maintain the count state, thus avoiding the hassle of manually passing state in asynchronous programming.

Implementing Function Currying

Implement the curry function of below code.

function sum(a, b, c) {
  return a + b + c;
}

const curriedSum = curry(sum);

curriedSum(1, 2, 3); // 6
curriedSum(1)(2, 3); // 6
curriedSum(1, 2)(3); // 6
curriedSum(1)(2)(3); // 6

Answer

function curry(fn) {
  return function curried(...args) {
    if (args.length >= fn.length) {
      return fn.apply(this, args);
    } else {
      return function(...args2) {
        return curried.apply(this, args.concat(args2));
      };
    }
  };
}

function sum(a, b, c) {
  return a + b + c;
}

const curriedSum = curry(sum);

console.log(curriedSum(1, 2, 3)); // 6
console.log(curriedSum(1)(2, 3)); // 6
console.log(curriedSum(1, 2)(3)); // 6
console.log(curriedSum(1)(2)(3)); // 6

Implement higher-order functions

Higher-order function for calculating execution time

function timingDecorator(fn) {
  return function() {
    console.time("timing");
    const result = fn.apply(this, arguments);
    console.timeEnd("timing");
    return result;
  };
}

const add = function(x, y) {
  return x + y;
};

const timingAdd = timingDecorator(add);
console.log(timingAdd(1, 2));

Higher-order function for caching return result

function memoizeDecorator(fn) {
  const cache = new Map();
  return function(...args) {
    const key = JSON.stringify(args);
    if (cache.has(key)) {
      return cache.get(key);
    }
    const result = fn.apply(this, args);
    cache.set(key, result);
    return result;
  };
}

const fibonacci = function(n) {
  if (n < 2) return n;
  return fibonacci(n - 1) + fibonacci(n - 2);
};

const memoizeFibonacci = memoizeDecorator(fibonacci);
console.log(memoizeFibonacci(10));

Implementing a deferred execution function

function delayDecorator(fn, delay) {
  return function() {
    const args = arguments;
    setTimeout(function() {
      fn.apply(this, args);
    }, delay);
  };
}

const sayHello = function(name) {
  console.log(`Hello, ${name}!`);
};

const delayedHello = delayDecorator(sayHello, 1000);
delayedHello("John");

Implementing generator

function makeGenerator(array) {
  let index = 0;
  return function() {
    if (index < array.length) {
      return { value: array[index++], done: false };
    } else {
      return { done: true };
    }
  };
}

const generator = makeGenerator([1, 2, 3]);

let result = generator();
while (!result.done) {
  console.log(result.value);
  result = generator();
}

In this example, we define a function called makeGenerator, which takes an array as its parameter and returns a new function. This new function uses closure to save the array index internally and returns each element of the array in sequence based on the index, until all elements are returned. We pass the array [1, 2, 3] to the makeGenerator function and assign the returned function to the variable generator. Then, we call the generator function to retrieve each element of the array one by one and output them to the console. In this way, we can easily use closure to implement generators and generate values one by one in a lazy manner, avoiding the performance and memory consumption issues caused by calculating all values at once. At the same time, using closure can maintain the state and scope of the function, avoiding problems such as global variable pollution and variable conflicts.

Implementing event listener

function createEventListener(element, eventName, handler) {
  element.addEventListener(eventName, handler);
  return function() {
    element.removeEventListener(eventName, handler);
  };
}

const button = document.getElementById("myButton");
const onClick = function() {
  console.log("Button clicked!");
};

const removeEventListener = createEventListener(button, "click", onClick);

setTimeout(function() {
  removeEventListener();
}, 5000);

In this example, we define a createEventListener function that takes a DOM element, an event name, and an event handler function as arguments and returns a new function. This new function uses closure to save the DOM element, event name, and event handler function internally and adds an event listener to the DOM element when executed. We pass a button element, a click event handler function, and the event name "click" to the createEventListener function and assign the returned function to removeEventListener. Then, we manually remove the event listener by calling the removeEventListener function after a certain time period to stop responding to button click events. This way, we can easily implement event listeners using closures and control their lifecycle in a flexible way, avoiding memory leaks and performance issues. Using closure also allows us to maintain the function's state and scope, avoiding global variable pollution and variable conflicts.

Reference: js闭包的理解 - 掘金 (juejin.cn)

JAVASCRIPT  USAGE  CLOSURE 

       

  RELATED


  0 COMMENT


No comment for this article.



  RANDOM FUN

Do you know the end?

You know the start, but do you know the end?