Variables, Scope and Types

A variable is a named place to remember a value between lines. This lesson covers how to declare one (const/let/var), where a name is visible (scope), which kinds of values exist (the primitive types), and the one copy-behaviour distinction that explains half of all beginner bugs: value versus reference.

What a variable is

A named binding to a value

A variable is a name that the engine attaches to a value. You create one with a declaration; afterwards the name stands in for the value everywhere in your code. Start with the strictest declaration and see what the engine says:

const clicks = 0;  // declare: create the name, give it a value
clicks = 1;        // try to rebind it — predict the result

You should see:

Uncaught TypeError: Assignment to constant variable.

const means "this name binds this value forever". Use let when the value genuinely must change:

let clicks = 0;
clicks = 1;           // fine — let allows reassignment
clicks = clicks + 10; // read the old value, compute, store back
console.log(clicks);  // 11

const is the default — here is why

The rule of thumb: declare everything const; switch to let only when reassignment is genuinely needed. A name that cannot be reassigned cannot be reassigned by accident — the engine checks a whole class of bug for you. var is the pre-2015 declaration: it works, but its scope rules surprise (below). You will meet it in older code; never write it.

The temporal dead zone

Declarations are hoisted — but not usable

The engine reads the whole block first and registers every name before running a line — that is hoisting. But const/let names stay unusable until their declaration line runs. The gap between "the name exists" and "the name works" is the temporal dead zone (TDZ):

console.log(user);   // runs BEFORE the declaration line below
const user = "Ada";

You should see:

Uncaught ReferenceError: Cannot access 'user' before initialization

var hoists differently — the classic trap

console.log(count);  // no error! prints...
var count = 5;

You should see undefined: var names are hoisted with the value undefined. A silent undefined is worse than a loud error — one more reason var is retired.

Scope

Block scope

A block is any { ... } pair — an if, a loop body, a function body. let and const names exist only inside the block where they were declared. Outside, the name never existed:

if (true) {
  const status = "active";
  console.log(status);   // works — inside the block
}
console.log(status);      // ReferenceError — status never existed out here
Diagram: nested scopes — block inside function inside global; a name lookup walks outward and stops at the first match

Scope is one-way glass: inner code sees outward through every level; outer code sees nothing in.

Shadowing

An inner block may declare a name that also exists outside. Inside, the inner one shadows the outer:

const name = "Ada";
if (true) {
  const name = "Grace";  // a DIFFERENT variable, temporarily hiding the first
  console.log(name);     // Grace
}
console.log(name);       // Ada — untouched

Legal, but in small scripts shadowing is more often a typo than a decision — if two visible variables share a name, rename one.

The primitive types

Seven types; everything else is an object

console.log(typeof "hi");          // string
console.log(typeof 5, typeof 5.5); // number, number — one type for all numbers
console.log(typeof true);          // boolean
console.log(typeof undefined);     // undefined
console.log(typeof null);          // "object" — a language bug, kept for compatibility
console.log(typeof 10n);           // bigint — huge integers
console.log(typeof Symbol());      // symbol — unique identifiers

null versus undefined: undefined means "never given a value"; null means "deliberately empty". And NaN ("not a number") is the result of broken arithmetic — type number, and famously not equal to itself; test for it with Number.isNaN(x).

Value versus reference — the half-of-all-bugs distinction

Diagram: assigning a primitive copies the value into a second independent box; assigning an object copies only the address, so both names reach the same object

Primitives copy the value; objects copy the address. One picture, dozens of bugs explained.

// Primitives: independent copies.
let a = 5;
let b = a;
b = 99;
console.log(a);       // 5 — untouched

// Objects: two names, ONE object.
const original = { score: 10 };
const alias = original;
alias.score = 99;
console.log(original.score); // 99 — "the other one" changed too

Nothing changed "behind your back": there is only one object, and both names are doors to it. Arrays, dates and every other non-primitive behave the same way.

Naming and reading style

Names that explain themselves

Names should make code readable without comments: attemptsLeft beats x; booleans read as yes/no questions (isLoggedIn, hasErrors). One concept, one name — resist data, info, temp.

Practice: name it, declare it, lose it

The task

Run foundations/09_values_and_types.js with node and predict every line first. Then write a block in the console where an inner shadowed name hides the outer one — and confirm the outer value after the block.

The checklist

  • You can explain const vs let in one sentence each — and why const is the default.
  • You have triggered the TDZ error and the var-hoisting undefined on purpose.
  • You can predict whether an assignment copies a value or an address.
  • You know typeof null lies, and how to test for NaN correctly.