Lesson 04 / 21
Variable Declarations
The difference between function-scoped and block-scoped declarations, redeclaration rules, the scope of constant binding, and the binding of the loop variable.
Contents
The previous lesson said new abilities are added beside the old one rather than in its place. The most concrete example of this is the declaration kinds: the language’s first form had a single declaration, ES2015 added two more, and the old one was not removed. The three do not do the same job.
The Programming Fundamentals course’s Scope and Lifetime lesson defined scope as the region of a program in which a name is visible. This lesson applies that definition to JavaScript and shows which region each of the three declarations chooses. The difference is not a matter of writing style; it determines where a name will be seen, when it becomes accessible, and whether it can be rebound.
Two Scope Units
A name declared with var is function-scoped: it is seen throughout the function it
sits inside. The declaration having been made inside a block changes nothing.
A name declared with let or const is block-scoped: it is seen only inside the
pair of curly braces it sits in, and inside the blocks within that.
function scopeExperiment() { if (true) { var varVariable = "with var"; let letVariable = "with let"; console.log(varVariable, "/", letVariable); } console.log(varVariable); console.log(typeof letVariable); } scopeExperiment();
with var / with let with var undefined
After the block closes, the name declared with var is still accessible; the name
declared with let has become invisible. This is why the third line is written with
typeof: reading it directly would throw an error.
Seeing the error itself shows how sharp the distinction is:
if (true) { let threshold = 20; console.log(threshold); } try { console.log(threshold); } catch (error) { console.log(error.name + ": " + error.message); }
20 ReferenceError: threshold is not defined
Block scope limits a name’s lifetime to the region it is needed in. The Programming Foundations course’s discussion of shadowing and name collision directly applies here: the narrower a name’s region of visibility, the lower the chance of it being overwritten by accident.
Redeclaration
The three declarations react differently to the same name being declared a second time.
var allows the same name to be redeclared in the same scope. The second declaration
does not create a new name; it assigns a new value to the existing one:
var threshold = 20; var threshold = 25; console.log(threshold);
25
This flexibility turns using the same name a second time, unnoticed, in the middle of a
long function, into a silent bug. let and const reject redeclaration in the same
scope:
let threshold = 20; let threshold = 25;
When this file is run, not a single line of it executes:
SyntaxError: Identifier 'threshold' has already been declared
Notice the kind of error. This is not a runtime error, it is a parsing error; the distinction established in the first lesson is at work here. The same name being declared twice is a condition that can be detected while the whole source is being read, so the program never starts at all.
A const declaration also requires an initial value:
const threshold;
SyntaxError: Missing initializer in const declaration
The reason for this will be seen in the next section: because a constant name can be bound only once, an unbound constant could never receive a value.
The Limit of a Constant Declaration
const fixes the binding. The Programming Fundamentals course’s Variables and
Binding lesson called the relationship between a name and a value “binding”; const
exactly prevents that relationship from changing.
const threshold = 20; try { threshold = 21; } catch (error) { console.log(error.name + ": " + error.message); }
TypeError: Assignment to constant variable.
What is fixed is the binding, not the value’s content. If the bound value is a mutable object, the object’s content can still change:
const record = { station: "A1", temperature: 21.5 }; record.temperature = 22; console.log(record);
{ station: 'A1', temperature: 22 }
The name is still bound to the same object; what changed is one of the object’s properties. Binding a different object to the same name, on the other hand, is rejected:
const record = { station: "A1", temperature: 21.5 }; try { record = { station: "A2", temperature: 19 }; } catch (error) { console.log(error.name + ": " + error.message); }
TypeError: Assignment to constant variable.
This distinction matches the terminology: const establishes a constant binding, it
does not make the value immutable. Actually freezing an object’s content is a
separate operation and is outside this course’s scope.
Binding the Loop Variable
Where the scope difference produces the most consequences is the loop header. A loop
variable declared with var is a single binding across the entire loop; a variable
declared with let is rebound on every iteration.
The difference is visible in functions produced inside the loop:
const varFns = []; for (var i = 0; i < 3; i++) { varFns.push(() => i); } console.log(varFns.map((f) => f())); const letFns = []; for (let j = 0; j < 3; j++) { letFns.push(() => j); } console.log(letFns.map((f) => f()));
[ 3, 3, 3 ] [ 0, 1, 2 ]
In the first loop, all three functions read the same i binding. By the time the loop
ends, that binding’s value is 3 — the loop terminated because the condition turned
false at this value. When the functions are called later, all of them see 3.
In the second loop, a separate j binding is created for each iteration, and each
function reads its own iteration’s binding. This behavior is a rule specific to the
let declaration in a loop header: a new binding is created at the start of each
iteration, initialized with the previous iteration’s value.
The arrow function syntax (() => i) is used here only for brevity; its behavior is
covered in the Arrow Functions lesson. A function keeping itself bound to the names of
the scope it was created in is called a closure; it was defined in the Programming
Foundations course and will be detailed in the next course.
const can be used when iterating over a collection, because each iteration
establishes a new binding and the variable is not reassigned within the iteration:
const temperatures = [21.5, 19.75, 23]; for (const value of temperatures) { console.log(value); }
21.5 19.75 23
Applying It to the Measurement Script
When the course’s axis script is rewritten according to these rules, which name is given which declaration, and why, becomes explainable:
const temperatures = [21.5, 19.75, 23, 18.25]; const threshold = 20; let total = 0; let overThreshold = 0; for (const value of temperatures) { total += value; if (value > threshold) { const difference = value - threshold; overThreshold += 1; console.log("threshold exceeded, difference:", difference); } } console.log("average:", total / temperatures.length); console.log("readings over threshold:", overThreshold);
threshold exceeded, difference: 1.5 threshold exceeded, difference: 3 average: 20.625 readings over threshold: 2
Three decisions were made together. Names that will not be rebound (temperatures,
threshold, the iteration variable value, the in-block difference) were declared
with const. Only the two names acting as accumulators (total, overThreshold) got
let. The name difference was declared only where it is needed, inside the if
block; thanks to block scope, it is invisible in the rest of the loop.
This writing convention will be kept throughout the course: every name that will not
be rebound is declared with const, every name that will be rebound with let. var
will be used only when its behavior is being demonstrated. The reasoning is not
aesthetic — once whether a name gets rebound can be read from its declaration, tracing
the code does not require scanning the entire body.
Summary
varis function-scoped;letandconstare block-scoped and are not visible outside the pair of curly braces.varcan be redeclared in the same scope;letandconstreject redeclaration with aSyntaxErrorat the parsing stage.constfixes the binding, it does not make the value immutable: an object’s properties can change, but a different object cannot be bound to the name.- A loop variable declared with
varis a single binding;letestablishes a new binding on every iteration, and functions produced inside the loop show this difference. - The course’s convention: a name that will not be rebound gets
const, a name acting as an accumulator getslet.
Next Step
Half of the scope question has been answered: where a name is seen. The other half is
when it becomes accessible. Touching the same name on the line before its declaration
gives undefined with var and throws an error with let; both are explained by the
fact that the declaration is “hoisted.” The next lesson covers lexical scope, hoisting,
and the rules of the region before a declaration.
To keep your progress and take notes, Log in
My notes
Log in to take notes.