Before you begin
Why this matters
Many bugs come from mixing up a name with its value: a value that stays because a list still holds it, or a variable that lives through every pass of a loop when each pass needed its own. Keeping the two questions apart lets you predict both.
What this helps you reason about
- Predict whether a value still exists after the name that made it is out of scope.
- Explain why setting a variable to null does not remove the value from a list.
- Spot a variable declared outside a loop whose value carries over from one pass to the next.
A fern is put on the watering list inside a block. After the block, the name fern is gone, but the fern is still on the list. This lesson separates the two questions behind that: where a name can be used, and how long a value exists.
Already know this? The short version
- Scope is textual and checked by the compiler (TS2304 Cannot find name); lifetime is a run-time question.
- An object lives while it is reachable: returned, pushed into an array, kept in a Map, or captured by a closure, it outlives the const that named it.
- Setting a variable to null ends nothing while another reference remains; the garbage collector reclaims an object some time after it becomes unreachable.
- A let or const declared in a loop body is a new variable on every pass; one declared before the loop lives through all of them.
Words in this lesson (6)
- scope
- The part of the program where a name can be used. For let and const, it is the nearest pair of braces, called a block.
- lifetime
- The time during a run when a value exists and can be used. It can be longer or shorter than the scope of a name that refers to the value.
- reference
- A value that leads to an object. A variable whose value is an object holds a reference, not a copy of the object, so copying the variable gives a second way to reach the same object.
- reachable
- A value is reachable when the program can still get to it by following references from the variables of running calls or from global variables.
- garbage collector
- The part of the JavaScript engine that finds values that are no longer reachable and reuses their memory, at a time it chooses.
- reference counting
- A way to reclaim memory by counting the references to each value and freeing the value when its count reaches zero. Values that refer to each other keep each other’s counts above zero, so counting alone never frees a cycle.
Examples are focused snippets; imports, enclosing functions, or classes may be omitted. C examples use C11; Haskell examples use Haskell 2010. Expand the notes for language-specific depth.
01 / Scope and lifetime
What is the difference between scope and lifetime?
Two questions sound alike but are different. Scope is about the text of the program: where can this name be used? Lifetime is about the run: when does this value exist?
The compiler answers the scope question before the program runs, by looking at blocks and declarations, as in Scope and shadowing. Below, fern can be used only inside its block. Using it after the closing brace does not compile.
Lifetime is decided while the program runs. The object that fern named was pushed into toWater, so the program can still reach it after the block. It still exists. In JavaScript, an object lives as long as it is reachable: as long as the program can still get to it by following references.
type Plant = { name: string; days: number };
const toWater: Plant[] = [];
{
const fern = { name: "fern", days: 2 };
toWater.push(fern);
}
// fern cannot be used here; toWater[0] is the same object
console.log(toWater[0].name); // fern Scope ends where the text says; a value lasts while something can still reach it.
Coming from C: There is no object that ends at a closing brace. Only the name ends there; the object stays while anything refers to it.
Precisely: what the specification says
The ECMAScript specification describes scope with environment records, created when a block or function starts. It says nothing about when memory is reclaimed. Engines may reclaim an unreachable object at any time, or never.
Experiment
Keep a value after its name’s block ends
The name fern can be used only inside its block. That is its scope, and the compiler checks it before the program runs. The value it named was put into toWater, so it still exists after the block: its lifetime is not tied to the name.
Why it matters When a name goes out of scope, ask what else still refers to its value. If something does, the value is still there.
Predict what the last line prints, and whether fern is still listed there. Then add the line console.log(fern.name); at the end and predict again.
type Plant = { name: string; days: number };
const toWater: Plant[] = [];
{
const fern = { name: "fern", days: 2 };
toWater.push(fern);
}
console.log(toWater[0].name);
What to look for
Before a line runs, a const shows not initialized yet. At the stop on toWater.push(fern), fern shows {name: "fern", days: 2}. At the console.log line, fern is no longer listed, and toWater shows [{name: "fern", days: 2}]. The run prints fern. The stepper may stop on the last line twice; that is normal for this debugger. After the edit, nothing runs: the compiler reports Line 7: Cannot find name 'fern'.
Expected output
fern02 / Outliving a name
How can a value outlive its name?
A value outlives its name whenever something with a longer lifetime still refers to it. There are three common ways:
- A function returns it. The local name ends with the call, as Call frames and the call stack showed, and the caller gets a reference to the same object.
- Code stores it in an array, a Map, or an object that lives longer.
- A closure uses it. Closures and captured variables showed that the closure keeps it.
A loop body is a block too. A let or const declared inside it is a new variable on every pass, with its own value. A variable declared before the loop is one variable for the whole loop, so a value put into it in one pass is still there in the next.
const boxes: string[][] = [];
for (const plant of ["fern", "ivy"]) {
const box: string[] = [];
box.push(plant);
boxes.push(box);
}
console.log(boxes); // [["fern"], ["ivy"]]
// with const box declared above the loop: one array
// shared by both passes, [["fern", "ivy"], ["fern", "ivy"]] A value outlives its name when it is returned, stored, or captured; a loop body makes a new variable each pass.
03 / Outliving a value
Can a name outlive its value?
In TypeScript, a reference can never outlive the object it leads to. While any reference exists, the object is reachable, so it is kept. That is the job of the garbage collector: it reclaims only objects that nothing can reach.
So the opposite mistake is the one to watch for. A value you meant to forget can stay alive for as long as something long-lived still refers to it, such as an array at the top level of a file.
const seen: Plant[] = [];
function water(plant: Plant): void {
seen.push(plant);
}
// every plant ever watered stays reachable through seen A reference always leads to a living object; the risk is keeping objects alive too long.
04 / The end
When does a value’s lifetime end?
A JavaScript object ends some time after it stops being reachable, not when a name goes out of scope. Setting a variable to null changes only that variable. If an array still holds the object, the object is still reachable.
Picture each reference as a hand holding the value. The value can go only after the last hand lets go, not the first. Some runtimes really count references this way, which is called reference counting. JavaScript engines do not count; they find what is still reachable. The rule for your code is the same: to let a value go, remove it from every place that keeps it. Reachability and garbage collection, a later lesson, shows how the collector finds them.
let fern: Plant | null = { name: "fern", days: 2 };
toWater.push(fern);
fern = null;
console.log(toWater[0].name); // fern: still reachable A value ends after the last reference to it is gone, not when one name stops referring to it.
Experiment
Set a name to null and look for its value
fern = null changes one name. The list still refers to the object, so the object is still reachable and still exists.
Why it matters Setting a name to null ends nothing while another reference remains. To let a value go, remove it from every place that keeps it.
Predict both lines. Then change toWater.push(fern); to toWater.push({ name: "ivy", days: 7 }); and predict again.
type Plant = { name: string; days: number };
const toWater: Plant[] = [];
let fern: Plant | null = { name: "fern", days: 2 };
toWater.push(fern);
fern = null;
console.log(toWater.length);
console.log(toWater[0].name);
What to look for
At the stop after the push, fern and toWater both show {name: "fern", days: 2}. After fern = null, fern shows null while toWater still shows [{name: "fern", days: 2}]. The run prints 1, then fern. After the edit, toWater shows [{name: "ivy", days: 7}], and after fern = null nothing refers to the fern object. The stepper cannot show when the garbage collector takes it. The run prints 1, then ivy.
Expected output
1
fern05 / Try it
Can you explain a fresh example?
Follow the object, not the names. The next lesson, Stack and heap, shows where values are stored while a program runs.
type Plant = { name: string; days: number };
const shelf: Plant[] = [];
function addPlant(name: string): Plant {
const plant = { name: name, days: 3 };
shelf.push(plant);
return plant;
}
let cactus: Plant | null = addPlant("cactus");
cactus.days = 14;
cactus = null;
console.log(shelf[0].name, shelf[0].days);
What is printed?
Reveal the reasoning
cactus 14. plant goes out of scope when addPlant returns, but the object was pushed into shelf and returned. cactus refers to the same object, so cactus.days = 14 changes the object in shelf. cactus = null changes only the variable; shelf still reaches the object.
Practice tickets
- SCOV-101 · Each lunch box also lists the earlier children’s sandwiches → Free
- SCOV-102 · The porch light goes off while someone is still out → Sign in · Pro
- SCOV-103 · The photo frame shows some photos twice and out of order → Sign in · Pro