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
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)