Declaring variables

A variable is a name for a value. In JavaScript you declare a variable with a keyword before you use it. Giving it its first value is initialization; giving it a new value later is assignment:

let openTickets;            // declaration: the value is undefined
console.log(openTickets);

openTickets = 14;           // first assignment (initialization)
openTickets = openTickets + 3;
console.log(openTickets);

const region = "North";     // declaration and initialization in one
console.log(region, openTickets);
undefined
17
North 17

There are three keywords for declaring variables. The first two arrived with ES2015 and are the ones to use; the third is the original and appears in older code.

let

let declares a variable that you can assign again, and that exists only inside the block { … } where it is declared. Use it for counters, running totals and anything else that changes.

const

const declares a variable that must be initialized at once and cannot be assigned again. It is block-scoped, like let. Assigning to it again throws a TypeError:

const VAT_RATE = 0.21;
console.log("VAT rate:", VAT_RATE);
VAT_RATE = 0.09;
VAT rate: 0.21
TypeError: Assignment to constant variable.

What is constant is the binding between the name and the value, not the value itself. A const object or array can still be changed inside; you just cannot make the name point to a different one:

const ticket = { id: "INC-20417", status: "open" };
ticket.status = "resolved";          // allowed: changes the object
console.log(ticket);

const readings = [118.2, 119.0];
readings.push(121.4);                // allowed: changes the array
console.log(readings);
{ id: 'INC-20417', status: 'resolved' }
[ 118.2, 119, 121.4 ]

Most style guides say: use const by default and switch to let only when you need to assign again. A reader then knows that every const name keeps the same value, which makes code easier to follow. (Constants in general are covered in Types and Type Systems.)

var

var is the original way to declare a variable, from before ES2015. It still works, but it behaves in ways that surprise people:

  • It is function-scoped, not block-scoped: a var declared inside an if or a loop is visible in the whole function.
  • It is hoisted with the value undefined, so you can use it before the line that declares it without an error (see below).
  • You can declare the same name twice with var without an error, which hides mistakes.
  • At the top level of a classic browser script, a var becomes a property of the global object (window), where other scripts can overwrite it.

New code uses let and const. You still need to read var, because you will meet it in older code, in tutorials and on the exam.

Naming rules

A variable name (an identifier) may contain letters, digits, _ and $, but must not start with a digit. Names are case-sensitive: total and Total are two different variables. Reserved words such as const, class, return and typeof cannot be used as names.

The convention is camelCase for variables and functions (openTickets, pricePerKwh), PascalCase for classes (MeterReading), and often UPPER_SNAKE_CASE for fixed configuration values (VAT_RATE).

Scope, hoisting and shadowing

The scope of a variable is the part of the program where its name can be used. let and const live in the nearest block; var ignores blocks:

function checkMeter(kwh) {
  if (kwh > 100) {
    var level = "high";   // var: visible in the whole function
    let note = "check";   // let: visible only inside this block
  }
  console.log(level);
  console.log(typeof note);
}
checkMeter(140);
high
undefined

level leaks out of the if block; note does not exist outside it, so typeof note reports "undefined". Block scope is the predictable behavior and the reason let replaced var. Scope inside functions is covered in depth in Functions.

Hoisting and the temporal dead zone

Before a scope runs, the engine registers all declarations in it. This is called hoisting, as if the declarations were lifted to the top. What you can do with a hoisted name depends on the keyword. A var is created with the value undefined:

console.log(meterState);   // no error: var is hoisted with undefined
var meterState = "online";
console.log(meterState);
undefined
online

A let or const is hoisted too, but cannot be used until the line that declares it has run. The stretch before that line is the temporal dead zone, and using the name there throws an error that points straight at the mistake:

console.log(meterState);   // in the temporal dead zone
let meterState = "online";
ReferenceError: Cannot access 'meterState' before initialization

Function declarations are hoisted completely, so you can call them before they appear in the file; see Functions.

Shadowing

A variable declared in an inner scope may have the same name as one in an outer scope. Inside the inner scope the new variable shadows (hides) the outer one; outside it, the outer one is visible again and unchanged:

const region = "North";

{
  const region = "South";   // a new variable that shadows the outer one
  console.log("inside:", region);
}

console.log("outside:", region);
inside: South
outside: North

Shadowing is legal, but it makes code harder to read: a reader has to work out which region is meant. Linters can warn about it.

Primitive types and objects

Every value in JavaScript is either a primitive or an object. A primitive value is a simple value that is not an object and has no properties of its own; it is immutable, so operations on it produce a new value instead of changing it. There are seven primitive types:

Type Example values Notes
string "North", 'INC-20417', `${n} kWh` Text; three kinds of quotes. See Strings
number 42, 0.31, -7.5, NaN, Infinity One type for whole numbers and decimals (a 64-bit floating-point number). See Numbers
bigint 9007199254740993n Whole numbers of any size; written with an n at the end
boolean true, false Lowercase, unlike Python's True/False
undefined undefined No value has been set
null null Deliberately empty
symbol Symbol("id") A unique identifier, mostly for object keys

Everything else is an object: plain objects { … }, arrays, functions, dates, maps, errors. An object is a collection of properties (named values) that can be changed. Objects are covered in Objects and JSON and arrays in Arrays.

Primitives seem to have methods — "north".toUpperCase() works — but that is a convenience: the engine briefly wraps the primitive in a matching object (String, Number, Boolean) to call the method, and the result is a new primitive. The original never changes.

Symbol

A symbol is a primitive value that is guaranteed to be unique. You create one with Symbol(), optionally with a description that only helps when debugging. Two symbols with the same description are still different:

const internalId = Symbol("internalId");
const otherId = Symbol("internalId");
console.log(internalId === otherId);
console.log(internalId.description, typeof internalId);

const meter = { serial: "KE-4471", [internalId]: 7781 };
console.log(Object.keys(meter));
console.log(meter[internalId]);
false
internalId symbol
[ 'serial' ]
7781

Symbols are used as object keys that can never clash with other keys and that ordinary loops and Object.keys() skip. You will rarely create your own as a beginner, but the language uses built-in ones such as Symbol.iterator, which makes an object usable in a for…of loop.

undefined and null

Most languages have one value for "nothing" (Python has None). JavaScript has two, and they have different meanings.

undefined

undefined is the value of something that has not been given a value. The engine produces it on its own in several places:

let assignedTo;                       // declared, never assigned
const ticket = { id: "INC-20417" };

function close(id, note) {
  return note;                        // note was not passed
}

function log(message) {
  console.log(message);               // no return statement
}

console.log(assignedTo);
console.log(ticket.priority);         // property that does not exist
console.log(close("INC-20417"));
console.log(log("Closing ticket"));
undefined
undefined
undefined
Closing ticket
undefined

Note that reading a property that does not exist is not an error, unlike a missing key in a Python dictionary. It quietly gives undefined, which can travel through a program a long way before it causes a visible problem. Reading a property of undefined is an error, though — one of the most common TypeErrors you will see. The operators ?. and ?? handle this gracefully; see optional chaining.

null

null is a value you set on purpose to say "there is no value here". The engine never produces it on its own in the cases above; it comes from your code or from an API that tells you "nothing found". For example, document.querySelector() returns null when no element matches.

const ticket = {
  id: "INC-20417",
  assignedTo: null,        // known: nobody is assigned yet
};

console.log(ticket.assignedTo);   // null: deliberately empty
console.log(ticket.closedAt);     // undefined: never set at all
console.log(ticket.assignedTo == ticket.closedAt);
console.log(ticket.assignedTo === ticket.closedAt);
null
undefined
true
false

The two are loosely equal (==) but not strictly equal (===); the operators page explains why. A practical convention: let the engine produce undefined, and use null yourself when you want to say "empty". JSON has null but no undefined, which is one more reason to use null for data.

The typeof operator

typeof is an operator that returns the type of a value as a string. It is written before the value, without parentheses (though typeof(x) also works):

console.log(typeof "INC-20417");
console.log(typeof 312.5);
console.log(typeof 10n);
console.log(typeof true);
console.log(typeof undefined);
console.log(typeof Symbol("id"));
console.log(typeof { id: 1 });
console.log(typeof [1, 2, 3]);
console.log(typeof function () {});
console.log(typeof null);
console.log(typeof notDeclaredAnywhere);
string
number
bigint
boolean
undefined
symbol
object
object
function
object
undefined

Three results need explaining:

  • Arrays are objects, so typeof cannot tell them apart. Use Array.isArray(value).
  • Functions report "function", even though they are objects too. That is handy for checking whether something can be called.
  • typeof null is "object", although null is a primitive. This is a bug from the first implementation in 1995: values carried a type tag, the tag for objects was 0, and null was stored as the null pointer, which is also 0. A proposal to change it was rejected. Check for null with value === null.

Also note the last line: typeof on a name that was never declared returns "undefined" instead of throwing an error. That makes it safe for checking whether something exists in this environment, such as typeof document.

Values and references

Assigning a primitive to another variable copies the value. The two variables are then independent. Assigning an object copies only a reference to it: both variables point to the same object, so a change through one name is visible through the other.

Left: primitives are copied. After let a = 14 and let b = a, the names a and b each have their own box holding 14; changing b does not change a. Right: objects are shared. After const t1 = an object and const t2 = t1, the names t1 and t2 both have an arrow to one single object with id INC-20417 and status open. Primitive: the value is copied let a = 14; let b = a; a 14 b 14 Two separate values Object: the reference is copied const t2 = t1; t1 t2 id: 'INC-20417' status: 'open' One object, two names
A const or let holding an object holds a reference. Copying the variable never copies the object.
let a = 14;
let b = a;
b = b + 1;
console.log(a, b);           // primitives: independent

const t1 = { id: "INC-20417", status: "open" };
const t2 = t1;
t2.status = "resolved";
console.log(t1.status);      // objects: the same object

console.log({ id: 1 } === { id: 1 });   // two different objects
console.log(t1 === t2);                 // the same object
14 15
resolved
false
true

Comparison follows the same rule: two primitives are equal when their values are equal, but two objects are equal only when they are the same object. Making a real copy of an object is covered in Objects and JSON.

Dynamic typing and type conversion

JavaScript is dynamically typed: a variable has no type of its own, so the same let can hold a number now and a string later. The engine checks types only while the program runs. That is flexible, but it means a value of the wrong type is discovered late, often far from where it came from. TypeScript (see Code Quality) adds type checking before the code runs.

Converting values explicitly

Data from outside the program — a form field, a command-line argument, a CSV file — almost always arrives as a string. Convert it yourself with the functions Number(), String() and Boolean(). Called without new, they return a primitive of that type (this is called casting):

const fromForm = "312.5";

console.log(Number(fromForm) + 10);     // convert first, then add
console.log(fromForm + 10);             // + with a string joins text

console.log(Number(""), Number("  42 "), Number("12 kWh"));
console.log(parseInt("12 kWh"), parseFloat("0.31 EUR"));
console.log(String(1250), String(null), String(true));
console.log(Boolean(""), Boolean("0"), Boolean(0), Boolean("no"));
322.5
312.510
0 42 NaN
12 0.31
1250 null true
false true false true

Points worth remembering from this example:

  • Number() is strict about the whole string: any extra text gives NaN ("not a number"), but an empty string gives 0, a trap with empty form fields.
  • parseInt() and parseFloat() read a number from the start of a string and stop at the first character that does not fit. Useful for "12px", risky for input you expect to be clean. Give parseInt() the base explicitly when it matters: parseInt("08", 10).
  • Boolean() is false only for the falsy values ("", 0, NaN, null, undefined and false); every non-empty string, including "0" and "no", is true. The full list is on the operators page.

Conversions that the engine does on its own, such as "5" * 2 becoming 10, are called type coercion and are the subject of Operators, Comparison and Type Coercion.