Variables, Values and Types
How JavaScript stores data: let, const and the older var, the primitive types and objects, typeof, and the difference between undefined and null.
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
vardeclared inside anifor 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
varwithout an error, which hides mistakes. -
At the top level of a classic browser script, a
varbecomes 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
typeofcannot tell them apart. UseArray.isArray(value). -
Functions report
"function", even though they are objects too. That is handy for checking whether something can be called. -
typeof nullis"object", althoughnullis a primitive. This is a bug from the first implementation in 1995: values carried a type tag, the tag for objects was 0, andnullwas stored as the null pointer, which is also 0. A proposal to change it was rejected. Check for null withvalue === 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.
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 givesNaN("not a number"), but an empty string gives0, a trap with empty form fields. -
parseInt()andparseFloat()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. GiveparseInt()the base explicitly when it matters:parseInt("08", 10). -
Boolean()isfalseonly for the falsy values ("",0,NaN,null,undefinedandfalse); every non-empty string, including"0"and"no", istrue. 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.