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.
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