Q1If you call countDownload() twice and then display total at the outermost level, what happens?
Closures — Functions That Remember Outer Variables
Closures are functions that remember variables from an outer scope. Covers counters that persist across calls, function factories, and functions made in loops.
Sometimes you want a function to hold a value that grows each time you call it, like a page's view count. If you count with an ordinary variable, though, either the value doesn't stick around, or any line can overwrite it.
This article covers closures, functions that remember variables from an outer scope, and function factories, functions that return a function with a setting built in.
Keeping a Value Between Calls — Closure Basics
Say you want to count downloads of a document. If you declare a let inside the counting function, it goes back to 0 on every call. If you declare it at the outermost level, the value sticks around, but now it sits where any line can overwrite it.
With a closure (a function that remembers variables from an outer scope), you can keep a value between calls while stopping lines outside from overwriting it. If you declare a variable inside a function and return a function that reads and writes it, the returned function takes that variable with it.
function makeDownloadCounter() {
let total = 0; // A variable that exists only inside makeDownloadCounter
const countUp = () => {
total = total + 1; // Updates the outer total
return total;
};
return countUp; // Returns the function itself
}
const countDownload = makeDownloadCounter(); // Makes one counter
console.log(countDownload()); // 1
console.log(countDownload()); // 2
console.log(countDownload()); // 3 (picks up from the previous value)
// console.log(total); // ReferenceError: total is not defined
const countDownload = makeDownloadCounter()— holds the returned functionconsole.log(total)— the name isn't visible, soReferenceError
let total = 0— callingcountDownload()doesn't reset it to 0return countUp— returns the function itself, not a value
total = total + 1— updates the outer total- Can still read and write this total after the outer function has finished running
countUp is returned to the outermost level and still remembers the outer total. Even after the function finishes, the remembered variable stays around.The function named countUp on the inside is called as countDownload on the outside. What return hands over is the function itself, so you can give the receiving variable whatever name fits where you use it. The table below compares where you put the counted value, whether it survives between calls, and whether it can be overwritten.
| Where the Count Lives | Value Between Calls | Can Outside Lines Overwrite It? |
|---|---|---|
| Declare let inside the counting function | Gone when the call ends; starts from 0 next time | No, the name isn't visible |
| Declare let at the outermost level | Kept | Yes, from any line |
| Declare let in an outer function and return an inner function | Continues from the previous value on each call to the returned function | Changes only through the returned function |
Making Functions with a Setting Built In — Function Factories
Say the point rate differs by membership rank. If you pass the rate as an argument on every calculation, callers can mix up the ranks, and you end up writing the same rate over and over. Here, you'd rather set the rate once, ahead of time.
With a function factory (a function that takes a setting and returns a function that remembers it), you only pass the rate once. A closure remembers arguments too, so each returned function keeps reading the rate it was created with.
function makePointCalculator(rate) {
// rate is an argument. The returned function keeps remembering it
return (amount) => Math.floor(amount * rate);
}
const regularPoint = makePointCalculator(0.01); // Regular members get 1%
const goldPoint = makePointCalculator(0.05); // Gold members get 5%
const platinumPoint = makePointCalculator(0.1); // Platinum members get 10%
console.log(regularPoint(3000)); // 30
console.log(goldPoint(3000)); // 150
console.log(platinumPoint(3000)); // 300
// Skip the variable and calculate on the spot with two pairs of parentheses
console.log(makePointCalculator(0.05)(5000)); // 250
rate each one remembers. Each function keeps reading the rate you passed in, unless something reassigns it.In makePointCalculator(0.05)(5000), the first pair of parentheses creates a function that remembers the rate, and the second pair passes in the amount and calculates on the spot. With only one pair of parentheses, what comes back is a function, not a calculated number.
A New Variable Every Time — Factories and let
Say you want to count down the remaining stock for each product. If you set up just one shared variable for the subtraction, reserving shirts also eats into the remaining caps. Each product needs its own value.
As with makeDownloadCounter from the start of this article, if you declare let inside a function that returns a function, that variable is created fresh every time you call the outer function. The returned functions don't share variables, so even though they come from the same definition, each one reads and writes only its own.
function makeStockReserver(stock) {
let rest = stock; // Created fresh on each call
return (count) => {
rest = rest - count;
return rest;
};
}
const reserveShirt = makeStockReserver(10);
const reserveCap = makeStockReserver(4);
console.log(reserveShirt(3)); // 7
console.log(reserveCap(1)); // 3
console.log(reserveShirt(2)); // 5 (only the shirt stock goes down)
console.log(reserveCap(1)); // 2
rest. Lowering one doesn't touch the other's remaining stock.reserveShirt(2) in the third console.log lowers only the shirt stock, from 7 to 5, and the cap side stays at 3. The next reserveCap(1) returns 2, which shows that the cap function keeps its own count as well.
Calling the Factory Every Time Starts Over
If you write makeStockReserver(10)(3) twice, both return 7. That's because each call creates a separate rest starting from 10. To carry the reduced stock over to the next reservation, put the returned function in a variable like reserveShirt, and call that variable.
Creating Functions in a Loop — A Separate let for Each Iteration
Say you want to set up a batch of output functions, one per log level. If you create functions inside a loop and store them in an array, you call them after the loop has finished. Which iteration's value each function remembers then depends on how you declared the loop variable.
This course doesn't use var, but putting it next to let makes the difference clear. With let in the for parentheses, a new variable is created for each iteration, and a function created in that iteration remembers that variable. var shares a single variable across all iterations, so every function reads the value left after the loop.
const levels = ["info", "warn", "error"];
// var shares one variable across every iteration
const varLoggers = [];
for (var i = 0; i < levels.length; i++) {
varLoggers.push(() => `[${levels[i]}]`);
}
console.log(varLoggers[0]()); // [undefined]
// let creates a new variable for each iteration
const letLoggers = [];
for (let j = 0; j < levels.length; j++) {
letLoggers.push(() => `[${levels[j]}]`);
}
console.log(letLoggers[0]()); // [info]
console.log(letLoggers[2]()); // [error]
var side, all three functions read the same variable; on the let side, each reads its own iteration's value. What a function remembers depends on whether a new variable is created each iteration.The var side gives [undefined] because i is 3 once the loop exits, and levels[3] doesn't exist. On the let side, levels[j] is also read at call time, but the j it reads is the variable created for that iteration, so each iteration's value is still there.
A Closure Doesn't Remember the Value at Creation Time
What a closure remembers is the variable itself, and it reads whatever value the variable holds when called. If you create a function that reads let rate = 0.05; and then reassign rate = 0.1;, that function calculates with 0.1 too. For a setting you want fixed, pass it as a factory argument, as in makePointCalculator(0.05).
Knowledge Check
Answer each question one by one.
Q2reserveCap starts with 4 in stock. After reserveShirt takes away 3, you pass 1 to reserveCap. What does it return?
Q3If you call a function created inside for (var i = 0; ...) after the loop has finished, what is i?