---
title: 'Variable Declarations'
source: 'https://academia.sh/en/courses/javascript-fundamentals/variable-declarations'
course: 'JavaScript Fundamentals'
language: en
updated: '2026-08-23T07:00:57+00:00'
license: 'CC BY-SA 4.0'
---

# 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.

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.

```js
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:

```js
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:

```js
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:

```js
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:

```js
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.

```js
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:

```js
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:

```js
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:

```js
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:

```js
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:

```js
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.
