Skip to content
academia.sh

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

  • var is function-scoped; let and const are block-scoped and are not visible outside the pair of curly braces.
  • var can be redeclared in the same scope; let and const reject redeclaration with a SyntaxError at the parsing stage.
  • const fixes 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 var is a single binding; let establishes 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 gets let.

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.

Start typing to search.

↑↓ Esc navigate · open · close