JavaScript
How JavaScript works
JavaScript is single-threaded and runs inside an engine (V8 in Chrome/Node, SpiderMonkey in Firefox). The engine compiles JS to bytecode via JIT compilation — there is no separate compile step you invoke manually.
Call stack & heap
The call stack tracks which function is currently executing. Each function call pushes a frame; returning pops it. The heap is unstructured memory where objects are allocated.
Event loop
Because JS is single-threaded, async work (timers, network, I/O) is handed off to Web APIs (browser) or libuv (Node). When the async work completes, the callback is queued. The event loop continuously checks: if the call stack is empty, it pulls the next callback from the queue and runs it.
There are two queues with different priorities:
- Microtask queue (higher priority) — Promise callbacks,
queueMicrotask. Drained completely before the next macrotask. - Macrotask queue —
setTimeout,setInterval, I/O callbacks. One task per event loop tick.
console.log("1");
setTimeout(() => console.log("2"), 0); // macrotask
Promise.resolve().then(() => console.log("3")); // microtask
console.log("4");
// Output: 1 4 3 2
// Reason: sync runs first, then microtasks, then macrotasksSyntax and variables
let, const, var
const— block-scoped, must be initialised, cannot be reassigned. Use by default.let— block-scoped, can be reassigned. Use when you need to reassign.var— function-scoped, hoisted to the top of its function. Avoid in modern code.
// var is hoisted — the declaration moves to the top of the function
console.log(x); // undefined (not an error)
var x = 5;
// let/const are hoisted but not initialised — Temporal Dead Zone
console.log(y); // ReferenceError
let y = 5;
// Block scope
{
let block = "only here";
const also = "only here";
var leaks = "outside too"; // leaks to the enclosing function
}
console.log(leaks); // "outside too"
console.log(block); // ReferenceErrorStrict mode
Adding "use strict"; at the top of a file or function opts in to stricter parsing: undeclared variables throw, silent failures become errors, and this in standalone functions is undefined instead of the global object. ES modules are always in strict mode.
Types and coercion
Primitive types
string— immutable sequence of UTF-16 code unitsnumber— 64-bit float (both integers and decimals); special values:Infinity,-Infinity,NaNbigint— arbitrary-precision integer, written withnsuffix:9007199254740993nboolean—trueorfalseundefined— variable declared but not assignednull— intentional absence of a valuesymbol— unique, immutable identifier
Everything else (arrays, functions, objects) is an object — a reference type. Variables hold a reference to the object in memory, not the object itself.
typeof "hello" // "string"
typeof 42 // "number"
typeof true // "boolean"
typeof undefined // "undefined"
typeof null // "object" ← famous bug, kept for compatibility
typeof {} // "object"
typeof [] // "object"
typeof function(){} // "function"
typeof Symbol() // "symbol"
typeof 42n // "bigint"== vs ===
=== (strict equality) checks value AND type — always use this. == (loose equality) coerces both sides to a common type first, producing surprising results.
0 == false // true (false coerced to 0)
"" == false // true
null == undefined // true
null === undefined // false
// Always use ===
0 === false // false
"1" === 1 // falseTruthy and falsy
The six falsy values: false, 0, "" (empty string), null, undefined, NaN. Everything else is truthy — including [], {}, and "0".
Nullish coalescing & optional chaining
// ?? — falls back only on null / undefined (not 0 or "")
const port = config.port ?? 3000;
// || — falls back on ANY falsy value (careful with 0, "")
const port2 = config.port || 3000; // wrong if port is 0
// ?. — short-circuits to undefined instead of throwing
const city = user?.address?.city;
const first = arr?.[0];
const result = obj?.method?.();Functions and scope
Declarations vs expressions vs arrow functions
// Declaration — hoisted, can be called before it's defined
function greet(name) {
return `Hello, ${name}`;
}
// Expression — not hoisted
const greet = function(name) {
return `Hello, ${name}`;
};
// Arrow — shorter syntax, no own 'this', not usable as constructor
const greet = (name) => `Hello, ${name}`;
const double = n => n * 2; // single param, no parens needed
const noop = () => {}; // no paramsDefault, rest, and spread
// Default parameters
function connect(host, port = 3000) {
return `${host}:${port}`;
}
// Rest — collects remaining args into an array
function sum(...nums) {
return nums.reduce((a, b) => a + b, 0);
}
sum(1, 2, 3, 4); // 10
// Spread — expands an iterable into individual elements
const a = [1, 2];
const b = [3, 4];
const merged = [...a, ...b]; // [1, 2, 3, 4]
Math.max(...merged); // 4Closures
A closure is a function that retains access to its outer scope even after the outer function has returned. This is how private state and factories are built in JavaScript.
function makeCounter() {
let count = 0; // private to makeCounter
return {
increment: () => ++count,
decrement: () => --count,
value: () => count,
};
}
const counter = makeCounter();
counter.increment(); // 1
counter.increment(); // 2
counter.value(); // 2this
this is determined by how a function is called, not where it's defined — except in arrow functions, which inherit this from their enclosing lexical scope.
const obj = {
name: "obj",
regular() { return this.name; }, // this = obj
arrow: () => this?.name, // this = outer scope (undefined in modules)
};
// Explicit binding
const fn = obj.regular;
fn(); // undefined (this = global / undefined in strict)
fn.call({name: "x"}); // "x"
fn.bind({name: "y"})(); // "y"Arrays
Creation and destructuring
const arr = [1, 2, 3];
const [first, second, ...rest] = arr; // first=1, second=2, rest=[3]
const copy = [...arr]; // shallow cloneMutating methods
arr.push(4); // add to end → returns new length
arr.pop(); // remove from end → returns removed element
arr.unshift(0); // add to start
arr.shift(); // remove from start
arr.splice(1, 2); // remove 2 elements starting at index 1
arr.splice(1, 0, 9); // insert 9 at index 1
arr.sort((a, b) => a - b); // in-place sort (always pass comparator for numbers)
arr.reverse(); // in-place reverseNon-mutating methods
const nums = [1, 2, 3, 4, 5];
nums.map(n => n * 2) // [2, 4, 6, 8, 10]
nums.filter(n => n % 2 === 0) // [2, 4]
nums.reduce((acc, n) => acc + n, 0) // 15
nums.find(n => n > 3) // 4
nums.findIndex(n => n > 3) // 3
nums.some(n => n > 4) // true
nums.every(n => n > 0) // true
nums.includes(3) // true
nums.indexOf(3) // 2
nums.slice(1, 3) // [2, 3] (non-mutating)
nums.flat() // flattens one level of nesting
nums.flatMap(n => [n, n * 2]) // map + flat in one pass
nums.join(", ") // "1, 2, 3, 4, 5"
// Static helpers
Array.from("hello") // ['h','e','l','l','o']
Array.from({length: 3}, (_, i) => i) // [0, 1, 2]
Array.isArray([]) // trueObjects
Literals, shorthand, and computed keys
const name = "Alice";
const age = 30;
// Shorthand properties — when key and variable name match
const user = { name, age };
// Computed keys
const key = "score";
const obj = { [key]: 100 }; // { score: 100 }
// Methods shorthand
const calc = {
value: 0,
add(n) { this.value += n; },
get double() { return this.value * 2; }, // getter
};Destructuring and spread
const { name, age, city = "Unknown" } = user; // default value
const { name: userName } = user; // rename
// Nested
const { address: { zip } } = user;
// Rest in object destructuring
const { name: n, ...rest } = user; // rest has everything except name
// Spread — shallow merge (last key wins)
const updated = { ...user, age: 31 };Object utility methods
Object.keys(obj) // array of own enumerable keys
Object.values(obj) // array of own enumerable values
Object.entries(obj) // array of [key, value] pairs
Object.assign({}, a, b) // shallow merge a and b into new object
Object.freeze(obj) // makes object immutable (shallow)
Object.fromEntries(entries) // inverse of Object.entries
// Iterate entries
for (const [key, val] of Object.entries(obj)) {
console.log(key, val);
}
// Check property existence
"name" in user // true (includes inherited)
user.hasOwnProperty("name") // true (own only)Collections
JavaScript has two purpose-built collection types — Map and Set — plus their weak-reference variants. They solve specific problems that plain objects and arrays handle poorly.
Map
A Map is an ordered collection of key-value pairs where any value (including objects and functions) can be a key. Unlike plain objects, insertion order is preserved and key lookup is always O(1).
const map = new Map();
// Set and get
map.set("name", "Alice");
map.set(42, "the answer");
map.set({ id: 1 }, "object key"); // objects can be keys
map.get("name"); // "Alice"
map.get(42); // "the answer"
map.has("name"); // true
map.size; // 3
map.delete("name");
map.clear();
// Initialise from entries
const scores = new Map([
["Alice", 95],
["Bob", 87],
["Carol", 92],
]);
// Iteration — always in insertion order
for (const [key, val] of scores) {
console.log(`${key}: ${val}`);
}
scores.keys(); // MapIterator of keys
scores.values(); // MapIterator of values
scores.entries(); // MapIterator of [key, value]
// Convert to/from object
const obj = Object.fromEntries(scores);
const back = new Map(Object.entries(obj));Prefer Map over a plain object when: keys are not strings, you need to iterate in insertion order, or you frequently add/delete keys (no prototype pollution risk).
Set
A Set stores unique values in insertion order. Duplicates are silently ignored. Membership testing is O(1) — far better than Array.includes which is O(n).
const set = new Set([1, 2, 3, 2, 1]); // {1, 2, 3} — duplicates removed
set.add(4);
set.has(2); // true
set.delete(2);
set.size; // 3
set.clear();
// Iteration
for (const val of set) console.log(val);
[...set]; // convert to array
Array.from(set); // same
// Common patterns
const dedup = arr => [...new Set(arr)];
dedup([1, 2, 2, 3, 3, 3]); // [1, 2, 3]
// Set operations (no built-ins — use spread)
const a = new Set([1, 2, 3]);
const b = new Set([2, 3, 4]);
const union = new Set([...a, ...b]); // {1,2,3,4}
const intersection = new Set([...a].filter(x => b.has(x))); // {2,3}
const difference = new Set([...a].filter(x => !b.has(x))); // {1}WeakMap & WeakSet
Weak variants hold weak references — they don't prevent the garbage collector from reclaiming keys. Keys must be objects (or registered symbols). They are not iterable and have no size property.
- WeakMap — ideal for storing private data or metadata associated with an object without leaking memory when the object is no longer used.
- WeakSet — useful for tracking a set of objects (e.g. "visited" nodes in a graph) without preventing garbage collection.
// WeakMap — private data per instance
const _private = new WeakMap();
class Person {
constructor(name) {
_private.set(this, { name });
}
getName() {
return _private.get(this).name;
}
}
const p = new Person("Alice");
p.getName(); // "Alice"
// When p is garbage collected, _private entry is automatically removed
// WeakSet — track visited objects
const visited = new WeakSet();
function process(node) {
if (visited.has(node)) return;
visited.add(node);
// ... process node
}Classes and prototypes
Class syntax
class Animal {
#name; // private field (ES2022)
constructor(name) {
this.#name = name;
}
get name() { return this.#name; }
speak() {
return `${this.#name} makes a sound.`;
}
static create(name) { // factory static method
return new Animal(name);
}
}
class Dog extends Animal {
#breed;
constructor(name, breed) {
super(name); // must call super before using this
this.#breed = breed;
}
speak() {
return `${this.name} barks!`; // override
}
}
const d = new Dog("Rex", "Husky");
d.speak(); // "Rex barks!"
d instanceof Dog; // true
d instanceof Animal; // truePrototype chain
Every object has an internal [[Prototype]] link. Property lookups walk the chain until the property is found or the chain ends at null. Classes are syntactic sugar over this mechanism.
// class syntax compiles down to this prototype pattern
function Animal(name) { this.name = name; }
Animal.prototype.speak = function() { return this.name; };
// Manual prototype chain
const proto = { greet() { return `Hi, I'm ${this.name}`; } };
const obj = Object.create(proto);
obj.name = "Alice";
obj.greet(); // "Hi, I'm Alice"
// Inspect chain
Object.getPrototypeOf(d) === Dog.prototype; // true
Object.getPrototypeOf(Dog.prototype) === Animal.prototype; // trueAsynchronous JavaScript
Callbacks and callback hell
// Callbacks work but nest poorly
fetchUser(id, (err, user) => {
if (err) return handleError(err);
fetchPosts(user.id, (err, posts) => {
if (err) return handleError(err);
fetchComments(posts[0].id, (err, comments) => {
// pyramid of doom
});
});
});Promises
A Promise represents a value that will be available in the future. It can be pending, fulfilled, or rejected.
const p = new Promise((resolve, reject) => {
setTimeout(() => resolve("done"), 1000);
});
p.then(val => console.log(val)) // "done"
.catch(err => console.error(err))
.finally(() => console.log("always"));
// Chaining — each .then returns a new Promise
fetch("/api/user")
.then(res => res.json())
.then(user => user.name)
.then(name => console.log(name))
.catch(console.error);
// Combinators
Promise.all([p1, p2, p3]) // rejects if any reject
Promise.allSettled([p1, p2, p3]) // waits for all, reports each outcome
Promise.race([p1, p2]) // settles with first to settle
Promise.any([p1, p2]) // fulfils with first to fulfilasync / await
async functions always return a Promise. await pauses execution of the async function until the awaited Promise settles — but does not block the thread.
async function getUser(id) {
try {
const res = await fetch(`/api/users/${id}`);
const user = await res.json();
return user;
} catch (err) {
console.error("fetch failed", err);
throw err; // re-throw so the caller can handle it too
}
}
// Run in parallel — do NOT await inside a loop for independent tasks
async function loadAll(ids) {
const promises = ids.map(id => fetch(`/api/users/${id}`));
const responses = await Promise.all(promises);
return Promise.all(responses.map(r => r.json()));
}Error handling
try / catch / finally
try {
JSON.parse("invalid json");
} catch (err) {
console.log(err instanceof SyntaxError); // true
console.log(err.name); // "SyntaxError"
console.log(err.message); // "Unexpected token..."
console.log(err.stack); // stack trace
} finally {
// always runs — use for cleanup
}Built-in error types
Error— base typeTypeError— wrong type (null.property)RangeError— value out of range (new Array(-1))ReferenceError— accessing undeclared variableSyntaxError— invalid code (JSON.parseof bad JSON)URIError— malformed URI indecodeURIComponent
Custom errors
class ValidationError extends Error {
constructor(message, field) {
super(message);
this.name = "ValidationError";
this.field = field;
}
}
function validate(user) {
if (!user.email) throw new ValidationError("Email required", "email");
}
try {
validate({});
} catch (err) {
if (err instanceof ValidationError) {
console.log(`Field: ${err.field}`);
} else {
throw err; // re-throw unexpected errors
}
}Async error handling
// async/await — use try/catch
async function run() {
try {
await riskyOperation();
} catch (err) {
console.error(err);
}
}
// Promise chain — use .catch()
riskyOperation()
.then(doSomething)
.catch(err => console.error(err));
// Unhandled rejections — always handle!
process.on("unhandledRejection", (reason) => console.error(reason)); // Node
window.addEventListener("unhandledrejection", e => console.error(e.reason)); // browserModules
ES modules (import / export) are the standard. They are always in strict mode, evaluated once and cached, and use static analysis — bundlers can tree-shake unused exports.
Named and default exports
// math.js
export const PI = 3.14159;
export function add(a, b) { return a + b; }
export function subtract(a, b) { return a - b; }
// default export — one per file
export default class Calculator { /* ... */ }
// ── importing ──
import Calculator from "./math.js"; // default
import { add, subtract } from "./math.js"; // named
import { add as plus } from "./math.js"; // rename
import * as math from "./math.js"; // namespace
import Calculator, { PI } from "./math.js"; // bothRe-exports and barrels
// index.js — barrel file that re-exports from multiple modules
export { add, subtract } from "./math.js";
export { default as Calculator } from "./math.js";
export * from "./utils.js";Dynamic import
import() is a runtime expression that returns a Promise. Use it for code splitting — loading a module only when it's actually needed.
// Load only when the user clicks
button.addEventListener("click", async () => {
const { Chart } = await import("./chart.js");
new Chart(canvas, data);
});
// Conditional loading
const locale = "fr";
const messages = await import(`./locales/${locale}.js`);