Closures in JavaScript — замыкания JavaScript
What is a Closure?
A Closure is a function that has access to variables from its outer (parent) scope, even after that outer function has finished executing.
In Simple Terms
A closure allows a function to "remember" the environment in which it was created and use variables from that environment later.
Basic Example
function outer() {
let counter = 0; // Variable from outer function
function inner() {
counter++; // Access to outer function variable
console.log(counter);
}
return inner;
}
const increment = outer();
increment(); // 1
increment(); // 2
increment(); // 3
What happens?
- Function
outercreates variablecounterand functioninner - Function
inneris returned and assigned toincrement - Although
outerhas finished executing,innerretains access tocounter - Each call to
increment()uses the same variablecounter
Key Point:
A closure is created automatically every time a function is created inside another function and has access to its variables.
How do Closures Work?
Closures work thanks to the Lexical Environment.
Scope Chain
let global = 'Global';
function outer() {
let outerVar = 'Outer';
function inner() {
let innerVar = 'Inner';
console.log(innerVar); // Access to innerVar
console.log(outerVar); // Access to outerVar (closure)
console.log(global); // Access to global
}
return inner;
}
const closure = outer();
closure();
Practical Examples
Counter with Private Data
function createCounter() {
let count = 0; // Private variable
return {
increment: function() {
count++;
return count;
},
decrement: function() {
count--;
return count;
},
getCount: function() {
return count;
}
};
}
const counter = createCounter();
console.log(counter.increment()); // 1
console.log(counter.increment()); // 2
console.log(counter.decrement()); // 1
console.log(counter.getCount()); // 1
// No direct access to count
console.log(counter.count); // undefined
Encapsulation:
Closures allow creating private variables in JavaScript that cannot be changed directly from outside.
Function Factory
function createMultiplier(multiplier) {
return function(number) {
return number * multiplier;
};
}
const double = createMultiplier(2);
const triple = createMultiplier(3);
console.log(double(5)); // 10
console.log(triple(5)); // 15
Each call to createMultiplier creates its own closure with its own multiplier value.
Event Listeners
function setupButtons() {
const buttons = document.querySelectorAll('button');
buttons.forEach((button, index) => {
button.addEventListener('click', function() {
console.log(`Button ${index} clicked`); // Closure on index
});
});
}
Each event handler creates a closure that remembers its index.
Classic Loop Problem
Problem
for (var i = 0; i < 3; i++) {
setTimeout(function() {
console.log(i);
}, 1000);
}
// Outputs: 3, 3, 3 (not 0, 1, 2)
Why? All three functions use the same closure with one variable i. By the time setTimeout executes, the loop is finished and i = 3.
Solution 1: Use let
for (let i = 0; i < 3; i++) {
setTimeout(function() {
console.log(i);
}, 1000);
}
// Outputs: 0, 1, 2
let creates a new variable on each iteration of the loop.
Solution 2: IIFE (pre-ES6)
for (var i = 0; i < 3; i++) {
(function(j) {
setTimeout(function() {
console.log(j);
}, 1000);
})(i);
}
// Outputs: 0, 1, 2
IIFE creates a new scope with its own copy of i.
Patterns with Closures
Module Pattern
const Calculator = (function() {
// Private variables and functions
let result = 0;
function log(operation, value) {
console.log(`${operation}: ${value}, result = ${result}`);
}
// Public API
return {
add: function(value) {
result += value;
log('Add', value);
return this;
},
subtract: function(value) {
result -= value;
log('Subtract', value);
return this;
},
getResult: function() {
return result;
}
};
})();
Calculator
.add(10)
.add(5)
.subtract(3);
console.log(Calculator.getResult()); // 12
Memoization
function memoize(fn) {
const cache = {}; // Private cache
return function(...args) {
const key = JSON.stringify(args);
if (key in cache) {
console.log('From cache');
return cache[key];
}
console.log('Computing');
const result = fn(...args);
cache[key] = result;
return result;
};
}
const expensiveOperation = memoize((n) => {
let sum = 0;
for (let i = 0; i < n; i++) {
sum += i;
}
return sum;
});
console.log(expensiveOperation(1000000)); // Computing
console.log(expensiveOperation(1000000)); // From cache
Currying
function curry(fn) {
return function curried(...args) {
if (args.length >= fn.length) {
return fn.apply(this, args);
}
return function(...nextArgs) {
return curried.apply(this, args.concat(nextArgs));
};
};
}
const sum = (a, b, c) => 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
React and Closures
"Stale Closure" Problem
function Counter() {
const [count, setCount] = useState(0);
useEffect(() => {
const timer = setInterval(() => {
// Will always use initial value count = 0
setCount(count + 1);
}, 1000);
return () => clearInterval(timer);
}, []); // Empty dependency array
return <div>{count}</div>;
}
Solution
function Counter() {
const [count, setCount] = useState(0);
useEffect(() => {
const timer = setInterval(() => {
// Uses current value
setCount(prevCount => prevCount + 1);
}, 1000);
return () => clearInterval(timer);
}, []);
return <div>{count}</div>;
}
Memory Leaks
Closures can lead to memory leaks if they hold large objects.
Memory Leak
function createHeavyClosure() {
const hugeArray = new Array(1000000).fill('data');
return function() {
console.log('Hello');
// Function doesn't use hugeArray, but it stays in memory
};
}
const fn = createHeavyClosure();
Solution
function createHeavyClosure() {
const hugeArray = new Array(1000000).fill('data');
// Use only what's needed
const dataLength = hugeArray.length;
return function() {
console.log('Array length:', dataLength);
// hugeArray can be garbage collected
};
}
Frequently Asked Questions
What will this code output?
function createFunctions() {
const functions = [];
for (var i = 0; i < 3; i++) {
functions.push(function() {
console.log(i);
});
}
return functions;
}
const funcs = createFunctions();
funcs[0](); // ?
funcs[1](); // ?
funcs[2](); // ?
Answer: All three calls will output 3, because all functions close over the same variable i, which equals 3 after the loop.
Conclusion
Closures are:
- Function + its lexical environment
- Access to outer function variables after its completion
- Foundation for modules, currying, memoization
- Way to create private data
- Potential source of memory leaks
- Cause of "stale" values in React hooks
In Interviews:
Be prepared to:
- Explain what a closure is in simple terms
- Provide practical use case examples
- Solve problems with loops and setTimeout
- Explain memory leak issues
- Show how closures work in React