Module 6 · Scope, Hoisting and Closures
Where a Name Lives: Scope and Closures, the Picture
In this lesson
- Say which scope a name belongs to (the file, a function or a block) and where in a program it can be seen.
- Follow a lookup outward, one scope at a time, until the name is found or the program stops with a
ReferenceError. - Explain what a closure is, and write a counter factory whose count no other code can touch.
Kenji's bookshop page counts its visitors in a variable called count, at the top of the file. Bob adds a like button and also needs a counter, so he writes count = 0 when the likes reset. Now every reset of the likes wipes out the day's visitors, and nobody sees an error. Two features share one name, and any line in the file can change it. This lesson gives each counter a private count that only its own code can reach.
Kenji's shared count
Here is the bug, cut down to a few lines. Kenji's addView and Bob's resetLikes both write the same count.
let count = 0;
function addView() {
count++;
}
function resetLikes() {
count = 0;
}
addView();
addView();
addView();
resetLikes();
addView();
console.log("views: " + count);
views: 1
Four visitors came, and the page says one. Nothing is wrong with either function on its own. The trouble is where count lives: at the top of the file, where every function can see it and change it. So the fix is not a better function. It is a better place for the name, a place only the counter's own code can reach. JavaScript calls that place a scope.
A scope is a box around lines of code
A scope is the part of a program where a name can be seen. Draw it as a box around some lines of code. A name declared inside the box belongs to that box. There are three kinds of box.
- The file. A name declared at the top level of a file can be seen anywhere in that file.
- A function. A name declared inside a function, and every parameter, can be seen only inside that function.
- A block. A block is a pair of curly braces, such as the body of an
ifor afor. Aletorconstdeclared inside it can be seen only inside those braces.
You have met the block already. In Module 4, the i of a for loop lived only inside the loop, and reading it afterwards stopped the program. The program below has all three boxes.
const shop = "Progsity Books";
function openShop() {
const hours = "9 to 5";
console.log(shop + " opens " + hours);
}
openShop();
if (shop.length > 5) {
const note = "long name";
console.log(note);
}
console.log(typeof hours, typeof note);
Progsity Books opens 9 to 5
long name
undefined undefined
shop lives in the file, so openShop can read it. hours lives in the function and note lives in the block. On the last line, outside both boxes, neither name exists. typeof answers "undefined" for a name that does not exist instead of stopping the program, which makes it a safe way to look. So a name can be seen inside its own box, and nowhere outside it.
Looking a name up, from the inside out
Boxes sit inside other boxes. A function's box sits inside the file's box, and a block inside a function sits inside the function's box. When a line uses a name, JavaScript runs a lookup. It searches the box the line is in, then the box around that one, and so on outward. The first box that has the name wins. If even the file does not have it, the program stops with a ReferenceError.
So an inner box can see every name in the boxes around it, and an outer box can see nothing inside an inner one. The next program shows both directions.
const rate = 0.05;
function addTax(price) {
const tax = price * rate;
return price + tax;
}
console.log(addTax(200));
console.log(tax);
210
ReferenceError: tax is not defined
Inside addTax, the lookup for rate found nothing in the function's box, stepped out, and found it in the file. On the last line, the lookup for tax started in the file, the outermost box, and had nowhere left to go. So a lookup walks outward and stops at the first match, or at the file with an error.
Each call makes a fresh box
A function's box is not made once, when the function is written. A new box is made every time the function is called, and its names start fresh in it. When the call ends, the box is usually thrown away. You saw calls stack up in Module 5. Each of those calls had a box of its own.
function visit(name) {
let cups = 0;
cups++;
console.log(name + " has had " + cups + " cup");
}
visit("Zara");
visit("Zara");
Zara has had 1 cup
Zara has had 1 cup
The second call does not remember the first. Its let cups = 0 ran again in a new box. So a name inside a function starts over on every call. That is exactly what Kenji's counter must not do, and the next section shows the way out.
A closure keeps its box alive
Here is the trick. A function made inside another function remembers the box it was made in. If the outer function returns that inner function, the outer call ends, but its box cannot be thrown away. The inner function still points at it. A function together with the box it remembers is called a closure.
Step through the program below. Watch the box of makeCounter() call 1. It survives after makeCounter returns, and count grows inside it on every call of ticket.
Here is the same program, run.
const shop = "Progsity Books";
function makeCounter() {
let count = 0;
return function next() {
count++;
return shop + " #" + count;
};
}
const ticket = makeCounter();
console.log(ticket());
console.log(ticket());
console.log(count);
Progsity Books #1
Progsity Books #2
ReferenceError: count is not defined
Three facts come out of it. count lives in the box of the makeCounter call, so it survives between calls of ticket. The file cannot read count, so no other code can reset it. And let count = 0 ran once, when makeCounter was called, not on every ticket. So a closure is a private variable that lives as long as the function that uses it.
Two calls, two counters
Every call of makeCounter makes a new box with a new count. So two counters from one factory never share a count, and Kenji and Bob can each have one.
So the factory is written once and called once per counter. Each call hands back a function that remembers its own box.
Three kinds of scope, and the counter factory
const a = 1; // the file's scope: seen by every line below
function outer(p) { // outer's scope: p and b, made fresh on every call
const b = 2;
if (b > 1) {
const c = 3; // a block's scope: c is seen only inside these braces
}
}
function makeCounter() { // the factory
let count = 0; // the private variable, one per call of makeCounter
return function () { // the inner function, which remembers this call's scope
count++;
return count;
};
}
const next = makeCounter(); // the closure: call it, and count survives between calls
- A lookup starts in the scope of the running line and walks outward; it never walks inward.
letandconstbelong to the nearest braces; parameters and names declared in a function belong to that function.- Every call of a function makes a new scope for that call.
- An inner function keeps the scope it was made in, for as long as the inner function itself can be reached.
The smallest scope: a const inside an if block.
const price = 450;
if (price > 400) {
const discount = 45;
console.log("pay " + (price - discount));
}
console.log("listed at " + price);
pay 405
listed at 450
discount exists only between the braces, and price, from the file, is seen in both places. Add console.log(discount) as the last line and the program stops with ReferenceError: discount is not defined.
Parameters and names declared in a function stay in its box, so two functions can use the same name without a clash.
function receipt(item, price) {
const total = price + price * 0.05;
return item + ": " + total;
}
function stock(item, count) {
const total = count * 12;
return item + ": " + total + " pieces";
}
console.log(receipt("pen", 20));
console.log(stock("pen", 3));
console.log(typeof total);
pen: 21
pen: 36 pieces
undefined
Each function has its own total, in its own box, and neither one leaks into the file. That is the reason to declare a name in the smallest box that needs it.
The program Kenji actually keeps. Each feature gets its own counter from one factory, and the reset touches only the likes.
function makeCounter() {
let count = 0;
return {
add: () => ++count,
reset: () => {
count = 0;
},
read: () => count,
};
}
const views = makeCounter();
const likes = makeCounter();
views.add();
views.add();
likes.add();
views.add();
likes.reset();
views.add();
console.log("views: " + views.read());
console.log("likes: " + likes.read());
views: 4
likes: 0
The factory returns three small functions in an object (objects are Module 8; here it is just three names in braces). All three were made in the same call, so they share that call's count. views and likes came from two calls, so they have two counts, and the reset of one cannot touch the other.
Where this is used
- React. A React component is a function, and its
useStatehook hands back the state and a setter. The click handlers written inside the component are closures over the values of the render that made them (lesson 04 shows where that bites). - Express.
express.static(root)serves files from a folder. It returns a middleware function that remembersrootand its options in a closure, and uses them on every request. - lodash. Its
onceanddebouncewrap your function in a new one. The wrapper keeps private state between calls, a call count in one and a pending timer in the other (lessons 03 and 06 build both). - Node itself. Node runs every CommonJS file inside a function, the "module wrapper" in its documentation. That is why a file's top-level names stay private to the file (Module 11).
Common mistakes
1. Reading a name after its block has closed.
const marks = 72;
if (marks >= 40) {
let result = "pass";
}
console.log(result);
ReferenceError: result is not defined
result lived in the if block, and the last line is outside it. Declare it before the block with let result; and set it inside. You will make this mistake because the block's closing brace looks like punctuation, not like a wall.
2. A factory that forgets to return the inner function.
function makeCounter() {
let count = 0;
function next() {
count++;
return count;
}
}
const ticket = makeCounter();
console.log(ticket());
TypeError: ticket is not a function
makeCounter made next and returned nothing, so ticket is undefined. Add return next;. You will forget it because writing the inner function feels like the end of the job.
3. Two counters that share one count.
let count = 0;
function makeCounter() {
return () => ++count;
}
const tea = makeCounter();
const coffee = makeCounter();
tea();
tea();
console.log("coffee " + coffee());
coffee 3
No error, and the wrong answer. count lives in the file, so both functions find the same one. Move let count = 0; inside makeCounter, so each call makes its own. You will do this because the first version of a counter is always written at the top of the file.
4. Calling the factory every time.
function makeCounter() {
let count = 0;
return () => ++count;
}
console.log(makeCounter()());
console.log(makeCounter()());
1
1
Each makeCounter() makes a new box with a new count of 0, so the count never grows. Call the factory once, keep the function it returns in a const, and call that. You will write it when the two pairs of brackets look like one call.
Amara logs how many minutes she studies each day. After every day, her app shows the average so far. Print that average after each day.
Input. The first line holds n. The second line holds n whole numbers, the minutes of each day.
Output. n lines: line i holds the average of the first i numbers, printed the way JavaScript prints a number.
Constraints. 1 <= n <= 100000. 0 <= each number <= 1000.
Sample. Input 4 and 30 45 0 60 gives 30, 37.5, 25 and 33.75 on four lines.
const input = require("fs").readFileSync(0, "utf8");
const tokens = input.split(/\s+/).filter(Boolean);
let at = 0;
const next = () => tokens[at++];
const nextInt = () => Number(next());
const out = [];
// your code: read with next() and nextInt(), push every line of output to out
console.log(out.join("\n"));
Graded as running-average. The hidden tests include n = 1, days of 0 minutes and averages that are not whole numbers.
Kenji's gym has k turnstiles, and each one keeps its own counter. Commands arrive one per line. Answer every get.
Input. The first line holds k and q. Then q commands: inc i, dec i, reset i or get i, where 1 <= i <= k. Every counter starts at 0. inc adds 1. dec subtracts 1, except that a counter at 0 stays at 0. reset sets it to 0.
Output. One line per get: the value of that counter.
Constraints. 1 <= k <= 1000. 1 <= q <= 100000. There is at least one get.
Sample. Input 2 9, then inc 1, inc 1, inc 2, dec 2, dec 2, get 1, get 2, reset 1, get 1 gives 2, 0 and 0 on three lines.
const input = require("fs").readFileSync(0, "utf8");
const tokens = input.split(/\s+/).filter(Boolean);
let at = 0;
const next = () => tokens[at++];
const nextInt = () => Number(next());
const out = [];
// your code: read with next() and nextInt(), push every line of output to out
console.log(out.join("\n"));
Graded as many-counters. The hidden tests include a dec on a counter at 0 and a single turnstile.
Zara records the temperature every hour and wants a "highest so far" line after each reading. Write a factory makeHighest() whose function takes a reading and returns the highest reading it has seen, then use it on the input.
Input. n, then n whole numbers, the readings.
Output. n lines: the highest reading among the first i.
Constraints. 1 <= n <= 100000. -50 <= each reading <= 60.
Sample. Input 5 and -3 -7 2 1 9 gives -3, -3, 2, 2 and 9 on five lines.
const input = require("fs").readFileSync(0, "utf8");
const tokens = input.split(/\s+/).filter(Boolean);
let at = 0;
const next = () => tokens[at++];
const nextInt = () => Number(next());
const out = [];
// your code: read with next() and nextInt(), push every line of output to out
console.log(out.join("\n"));
Not graded on its own. Decide what the private variable starts as. Then try an input where every reading is below 0.
Run in CompilerDavid's drawing app has a size slider with an undo button. Write a factory whose functions change a private size and remember every change, so undo can step back one change at a time.
Input. A start size s and q, then q commands. grow x adds x to the size, and shrink x subtracts x. undo takes back the most recent change that has not been undone yet, and does nothing when there is none. show prints the size.
Output. One line per show.
Constraints. 1 <= q <= 100000. 0 <= s <= 1000. 1 <= x <= 1000. There is at least one show.
Sample. Input 10 7, then grow 5, shrink 3, show, undo, show, undo, show gives 12, 15 and 10.
const input = require("fs").readFileSync(0, "utf8");
const tokens = input.split(/\s+/).filter(Boolean);
let at = 0;
const next = () => tokens[at++];
const nextInt = () => Number(next());
const out = [];
// your code: read with next() and nextInt(), push every line of output to out
console.log(out.join("\n"));
Not graded on its own. Keep the changes in a private array and take the last one off with pop() (Module 7 teaches it properly). Then try three undo lines in a row after only two changes.
Common doubts
Do I make a closure on purpose, or does it just happen?
It happens. Every function remembers the scope it was made in. It only matters when the function outlives the call that made it, as
ticketoutlivedmakeCounter. That is when people say "closure".Does the closure copy
count, or share it?It shares the variable itself, not a copy of its value. That is why the count can grow. In Example 3, the three functions made in one call all see the same
count, so a reset through one is seen by the other two.Why does
countnot go back to 0 on every call ofticket?Because
let count = 0is inmakeCounter, not in the inner function. It runs once per call ofmakeCounter. Callingticketruns only the inner function's two lines.Is the file's scope the same as the global scope?
Not in a Node file. Each file has its own top level, which is why one file's names do not leak into another. Lesson 02 shows the global object and why this track never writes to it.
Does a closure keep the whole outer scope in memory?
It keeps alive what it can still reach, for as long as the closure itself can be reached. Usually that costs a few bytes. Lesson 05 measures the case where it costs megabytes.
Key takeaways
- A scope is the part of a program where a name can be seen: the file, a function, or a block in braces.
- A lookup starts in the running scope and walks outward; it stops at the first match, or at the file with a
ReferenceError. - An outer scope can never see a name inside an inner one, which is what makes a name private.
- Every call of a function makes a fresh scope, so a function's names start over on every call.
- A closure is a function that keeps the scope it was made in; a counter factory uses one to give each counter a private count.
- Go deeper: Under the Hood, environment records, V8 contexts and a closure memory lab (Pro).
Next, lesson 02 takes every rule one at a time: the global object, var and its function scope, shadowing, hoisting and the dead zone.
End of lesson 1
Mark it done, and your progress moves with you.
Next: Every Rule, One by One: Blocks, var, Hoisting and the Dead Zone