JavaScript fundamentals: variables, types, functions and the equality rules that trip people up
The parts of JavaScript you use every day, explained precisely: let and const, the seven primitive types, how objects are references, functions and closures, and why === is almost always what you mean.
JavaScript is easy to start and hard to understand, because many of its rules are visible only when something goes wrong. This tutorial covers the fundamentals in the order you will meet them, and at each step says what the rule actually is, so that later surprises are not surprises. Everything here runs in any browser console (press F12 or Ctrl+Shift+J, Cmd+Option+J on a Mac) or in Node.js.
Declaring variables: const by default
const limit = 10; // cannot be reassigned
let count = 0; // can be reassigned
count = count + 1;
limit = 11; // TypeError: Assignment to constant variable.Use const unless you intend to reassign, then let. Both are block-scoped: they exist only inside the nearest pair of curly braces. var, the old keyword, is function-scoped and is "hoisted" so it can be read as undefined before its line runs; there is no reason to use it in new code.
const prevents reassigning the name, not changing the value. A const array can still be pushed to:
const list = [];
list.push('a'); // fine: the array changed, the binding did not
list = ['b']; // TypeErrorThe types
JavaScript has seven primitive types and one object type. typeof tells you which you have, with one famous mistake:
typeof 42 // "number"
typeof 42n // "bigint"
typeof "hi" // "string"
typeof true // "boolean"
typeof undefined // "undefined"
typeof Symbol() // "symbol"
typeof null // "object" <- a bug from 1995, kept for compatibility
typeof {} // "object"
typeof [] // "object" <- use Array.isArray(x)
typeof function(){} // "function" <- functions are objects with a special typeofPrimitives are copied by value; objects (including arrays and functions) are held by reference. This single fact explains most "my data changed unexpectedly" bugs:
let a = 1; let b = a; b = 2; // a is still 1
const p = { x: 1 }; const q = p; q.x = 2; // p.x is now 2: same object
const r = { ...p }; // shallow copy: a new objectNumbers are 64-bit floating point. There is no separate integer type, which is why 0.1 + 0.2 is 0.30000000000000004 (see the number tutorial) and why integers above 253 lose precision. undefined means "no value was ever assigned"; null means "deliberately empty". Functions that find nothing conventionally return null (document.querySelector) or undefined (array.find); check which.
Strings
Template literals, in backticks, allow interpolation and multiple lines and should be the default when you build strings:
const name = 'Ada';
const greeting = `Hello, ${name}. You have ${3 + 4} messages.`;Strings are immutable: every method returns a new string. s.toUpperCase() does nothing unless you keep the result. Indexing with s[0] gives UTF-16 code units, so emoji and many non-Latin characters have a length of 2; iterate with for (const ch of s) to get whole characters.
Equality
=== compares type and value with no conversion. == converts first, by rules nobody remembers:
0 == '' // true
0 == '0' // true
'' == '0' // false (so == is not even transitive)
null == undefined // true
null === undefined // false
NaN === NaN // false (use Number.isNaN)
[1] == 1 // trueUse === everywhere. The one idiom where == is deliberate is x == null, which is true for both null and undefined; even then, x === null || x === undefined or the newer x ?? fallback is clearer.
Objects compare by reference: {} === {} is false and [1,2] === [1,2] is false. To compare contents you compare the fields, or serialise with JSON.stringify for simple cases.
Truthiness
In a condition every value is converted to a boolean. Exactly these are false: false, 0, -0, 0n, "", null, undefined, NaN. Everything else is true, including "0", "false", [] and {}. This matters when 0 is a legitimate value:
const port = config.port || 3000; // bug: port 0 becomes 3000
const port = config.port ?? 3000; // only null/undefined fall throughFunctions
Three ways to write one, with one real difference:
function add(a, b) { return a + b; } // declaration: hoisted, has its own `this`
const add2 = function (a, b) { return a + b; }; // expression
const add3 = (a, b) => a + b; // arrow: no own `this`, implicit returnArrow functions take this from the surrounding code instead of from how they are called. That is what you want for callbacks inside methods and almost never what you want for object methods or event handlers that need this to be the element. Parameters can have defaults (function f(x = 1)) and the rest syntax collects extras into an array (function f(first, ...others)). A function with no return returns undefined.
Scope and closures
A function can see variables from where it was defined, not where it is called, and it keeps seeing them after the outer function has returned. That is a closure, and it is how JavaScript does private state:
function makeCounter() {
let n = 0;
return () => ++n; // closes over n
}
const next = makeCounter();
next(); next(); next(); // 3
const other = makeCounter();
other(); // 1: a separate nThe classic trap combined closures with var in loops: every callback saw the same final value. With let, each iteration gets its own binding, and the trap is gone:
for (let i = 0; i < 3; i++) setTimeout(() => console.log(i)); // 0 1 2Objects and arrays, briefly
const user = { name: 'Ada', tags: ['math'] };
user.email = 'ada@example.com'; // add a property
const { name, tags } = user; // destructuring
const [first] = tags;
const copy = { ...user, name: 'Grace' }; // new object with one change
delete user.email;
const nums = [3, 1, 2];
nums.map(n => n * 2); // [6, 2, 4] new array
nums.filter(n => n > 1); // [3, 2]
nums.reduce((sum, n) => sum + n, 0); // 6
nums.sort(); // sorts IN PLACE, and as strings: [1, 2, 3] here but [10, 9] -> [10, 9]
nums.sort((a, b) => a - b); // numeric sortsort, push, splice and reverse mutate the array; map, filter, slice and concat return new ones. The newer toSorted and toReversed are non-mutating versions available in all current browsers since 2023.
Pitfalls, collected
typeof null === "object"; check for null explicitly.==converts; use===.||treats 0 and "" as missing; use??for defaults.- Objects are references; spread or
structuredClone(obj)to copy. array.sort()without a comparator sorts numbers as strings.- Arrow functions have no own
this; do not use them for methods that need it. - A missing
returngivesundefined, silently.
Exercise
In the console, write a makeBank(opening) function that returns an object with deposit(n), withdraw(n) and balance() methods, with the balance held in a closure so it cannot be changed from outside. Make withdraw refuse to go below zero. Then predict, before running them, the results of [] + [], [] + {}, '5' - 2 and '5' + 2; run them; look up the conversion rule for any you got wrong.
Summary
constby default,letwhen reassigning, nevervar.- Seven primitives copied by value; objects by reference.
===,??,Array.isArray,Number.isNaN: the precise tools.- Closures capture the variables of the scope where a function was written.
Next: use the language on a real page with the DOM and events.