Introduction
JavaScript has a small set of core concepts that underpin everything else. Understanding closures, the prototype chain, and the event loop explains why JavaScript behaves the way it does — and prevents the bugs that come from misunderstanding it. See Javascript Guide for more context. See Javascript Guide for more context.
Closures
A closure is a function that remembers the variables from its outer scope even after that scope has finished executing.
function makeCounter(start = 0) {
let count = start; // this variable is "closed over"
return {
increment() { count++; },
decrement() { count--; },
value() { return count; },
};
}
const counter = makeCounter(10);
counter.increment();
counter.increment();
console.log(counter.value()); // 12
// count is not accessible from outside
console.log(counter.count); // undefined
Why closures matter: They enable private state, factory functions, and callbacks that remember context.
// Practical: event handlers that remember data
function attachHandlers(items) {
items.forEach((item, index) => {
item.addEventListener('click', () => {
console.log(`Clicked item ${index}: ${item.textContent}`);
// index and item are closed over — each handler remembers its own
});
});
}
// Practical: memoization
function memoize(fn) {
const cache = new Map();
return function(...args) {
const key = JSON.stringify(args);
if (cache.has(key)) return cache.get(key);
const result = fn.apply(this, args);
cache.set(key, result);
return result;
};
}
const expensiveCalc = memoize((n) => {
console.log('Computing...');
return n * n;
});
expensiveCalc(5); // Computing... → 25
expensiveCalc(5); // (from cache) → 25
The Prototype Chain
JavaScript uses prototypal inheritance. Every object has a [[Prototype]] link to another object. When you access a property, JavaScript walks up the chain until it finds it or reaches null.
const animal = {
breathe() { return `${this.name} breathes`; }
};
const dog = Object.create(animal);
dog.name = 'Rex';
dog.bark = function() { return 'Woof!'; };
console.log(dog.bark()); // "Woof!" — own property
console.log(dog.breathe()); // "Rex breathes" — found on prototype
console.log(dog.toString()); // "[object Object]" — found on Object.prototype
// The chain: dog → animal → Object.prototype → null
Object.getPrototypeOf(dog) === animal // true
Classes Are Syntactic Sugar
class Animal {
constructor(name) {
this.name = name;
}
breathe() {
return `${this.name} breathes`;
}
}
class Dog extends Animal {
bark() {
return 'Woof!';
}
}
const rex = new Dog('Rex');
rex.bark(); // "Woof!"
rex.breathe(); // "Rex breathes"
// Under the hood: same prototype chain
Object.getPrototypeOf(rex) === Dog.prototype // true
Object.getPrototypeOf(Dog.prototype) === Animal.prototype // true
The Event Loop
JavaScript is single-threaded but handles async operations through the event loop. Understanding this explains why setTimeout(fn, 0) doesn’t run immediately.
Call Stack Web APIs Task Queue Microtask Queue
───────────── ────────────── ───────────── ───────────────
main() setTimeout(...) callback1 promise.then(...)
↓ fetch(...)
↓ addEventListener
The order:
- Execute synchronous code (call stack)
- Process all microtasks (Promise callbacks, queueMicrotask)
- Process one macrotask (setTimeout, setInterval, I/O)
- Repeat
console.log('1'); // synchronous
setTimeout(() => console.log('2'), 0); // macrotask queue
Promise.resolve().then(() => console.log('3')); // microtask queue
console.log('4'); // synchronous
// Output: 1, 4, 3, 2
// Explanation:
// 1 and 4 run synchronously
// 3 runs next (microtask, before macrotasks)
// 2 runs last (macrotask)
// Practical implication: Promise.then runs before setTimeout
async function example() {
console.log('start');
await Promise.resolve(); // yields to microtask queue
console.log('after await'); // runs before any setTimeout
setTimeout(() => console.log('timeout'), 0);
console.log('end');
}
example();
// Output: start, after await, end, timeout
Async/Await and Promises
// Promise: represents a future value
const fetchUser = (id) => new Promise((resolve, reject) => {
setTimeout(() => {
if (id > 0) resolve({ id, name: 'Alice' });
else reject(new Error('Invalid ID'));
}, 100);
});
// async/await: syntactic sugar over Promises
async function getUser(id) {
try {
const user = await fetchUser(id);
return user;
} catch (err) {
console.error('Failed:', err.message);
return null;
}
}
// Parallel execution
async function getDashboard(userId) {
// BAD: sequential — 300ms total
const user = await fetchUser(userId);
const posts = await fetchPosts(userId);
const friends = await fetchFriends(userId);
// GOOD: parallel — ~100ms total
const [user2, posts2, friends2] = await Promise.all([
fetchUser(userId),
fetchPosts(userId),
fetchFriends(userId),
]);
}
Promise Combinators
// Promise.all: all must succeed, fails fast
const results = await Promise.all([fetch('/a'), fetch('/b'), fetch('/c')]);
// Promise.allSettled: wait for all, don't fail on errors
const results = await Promise.allSettled([fetch('/a'), fetch('/b')]);
results.forEach(r => {
if (r.status === 'fulfilled') console.log(r.value);
else console.error(r.reason);
});
// Promise.race: first to settle wins
const result = await Promise.race([
fetch('/primary'),
new Promise((_, reject) => setTimeout(() => reject(new Error('timeout')), 5000)),
]);
// Promise.any: first to succeed wins (ignores rejections)
const result = await Promise.any([fetch('/server1'), fetch('/server2')]);
this Binding
this is determined by how a function is called, not where it’s defined:
const obj = {
name: 'Alice',
greet() {
console.log(`Hello, ${this.name}`);
},
greetArrow: () => {
console.log(`Hello, ${this.name}`); // this = outer scope (undefined in strict mode)
},
};
obj.greet(); // "Hello, Alice" — method call, this = obj
const fn = obj.greet;
fn(); // "Hello, undefined" — plain call, this = undefined (strict mode)
// Fix with bind
const boundGreet = obj.greet.bind(obj);
boundGreet(); // "Hello, Alice"
// Arrow functions inherit this from enclosing scope
class Timer {
constructor() {
this.seconds = 0;
}
start() {
// Arrow function: this = Timer instance
setInterval(() => {
this.seconds++; // works correctly
}, 1000);
}
}
Destructuring and Spread
// Object destructuring
const { name, age, address: { city } = {} } = user;
const { name: userName, ...rest } = user; // rename + rest
// Array destructuring
const [first, second, ...remaining] = [1, 2, 3, 4, 5];
const [, , third] = [1, 2, 3]; // skip elements
// Function parameters
function display({ name, age = 0, role = 'user' } = {}) {
console.log(name, age, role);
}
// Spread
const merged = { ...defaults, ...overrides };
const combined = [...arr1, ...arr2];
const copy = [...original]; // shallow copy
Modules (ESM)
// math.js — named exports
export const PI = 3.14159;
export function add(a, b) { return a + b; }
export function multiply(a, b) { return a * b; }
// utils.js — default export
export default function formatDate(date) {
return date.toISOString().split('T')[0];
}
// app.js — importing
import formatDate from './utils.js'; // default import
import { add, multiply } from './math.js'; // named imports
import { add as sum } from './math.js'; // rename
import * as math from './math.js'; // namespace import
import formatDate, { add } from './combined.js'; // both
// Dynamic import (lazy loading)
const module = await import('./heavy-module.js');
JSON: Serialization and Parsing
// Serialize to JSON string
const json = JSON.stringify({ name: 'Alice', age: 30 });
// '{"name":"Alice","age":30}'
// Pretty print
JSON.stringify(obj, null, 2);
// Custom serialization
JSON.stringify(obj, (key, value) => {
if (key === 'password') return undefined; // exclude field
return value;
});
// Parse JSON string
const obj = JSON.parse('{"name":"Alice","age":30}');
// Safe parse
function safeParseJSON(str) {
try {
return { data: JSON.parse(str), error: null };
} catch (err) {
return { data: null, error: err.message };
}
}
// Limitations: JSON doesn't support Date, undefined, functions, Map, Set
const date = new Date();
JSON.stringify(date) // '"2026-03-30T10:00:00.000Z"' (string, not Date)
JSON.parse(JSON.stringify(date)) // string, not Date object
Semantic Versioning in package.json
{
"dependencies": {
"react": "^18.2.0",
"lodash": "~4.17.21",
"axios": "1.6.0"
}
}
| Notation | Meaning | Example |
|---|---|---|
^18.2.0 |
Compatible with 18.x.x | Allows 18.3.0, not 19.0.0 |
~4.17.21 |
Patch updates only | Allows 4.17.22, not 4.18.0 |
1.6.0 |
Exact version | Only 1.6.0 |
>=1.0.0 <2.0.0 |
Range | Any 1.x.x |
* |
Any version | Dangerous — avoid |
Resources
- MDN: JavaScript Guide
- JavaScript.info — comprehensive modern JS tutorial
- You Don’t Know JS (free)
- Airbnb JavaScript Style Guide
Comments