Why JavaScript

Brendan Eich wrote JavaScript in ten days in May 1995. Netscape needed a scripting language for their browser. It had to look like Java (marketing), act like Scheme (Eich’s preference), and be simple enough for non-programmers. The result was a language with first-class functions from Scheme, prototypal inheritance from Self, syntax from Java, and a collection of design mistakes that would haunt the web for decades.

None of that mattered. JavaScript became the only language that runs natively in every web browser on every device on Earth. There was no alternative and no competition. If you wanted to make a web page interactive, you used JavaScript. When Node.js arrived in 2009, JavaScript escaped the browser and colonized the server. Today, JavaScript is the most-used language on GitHub, the foundation of every web application, and the runtime behind billions of daily interactions.

“Any application that can be written in JavaScript, will eventually be written in JavaScript.”

— Atwood’s Law

I. Hello, World

In the browser (open your browser’s developer console with F12):

console.log("Hello, World");

In Node.js (save as hello.js):

console.log("Hello, World");
$ node hello.js Hello, World

Same code runs in both environments. The browser provides the DOM (document manipulation). Node.js provides the filesystem, network, and OS access. The core language is identical.

II. Variables and Types

// Three ways to declare variables const pi = 3.14159; // constant — cannot be reassigned let count = 0; // block-scoped, reassignable var old = "avoid this"; // function-scoped, hoisted — legacy // Always use const by default. Use let when you need to reassign. // Never use var.

Primitive Types

const str = "hello"; // string const num = 42; // number (64-bit float, like C's double) const big = 9007199254740993n; // bigint (arbitrary precision) const yes = true; // boolean const empty = null; // intentional absence of value const missing = undefined; // declared but not assigned const sym = Symbol("id"); // unique identifier // typeof (mostly makes sense) typeof "hello" // "string" typeof 42 // "number" typeof true // "boolean" typeof undefined // "undefined" typeof null // "object" — a famous bug from 1995, never fixed typeof {} // "object" typeof [] // "object" — arrays are objects

JavaScript has one number type: 64-bit IEEE 754 floating point. There are no integers. 1 and 1.0 are the same value. This means you lose integer precision above $2^{53}$ (about 9 quadrillion). For larger integers, use BigInt.

Equality

// == (loose equality) — performs type coercion. AVOID. 0 == "" // true (wat) 0 == false // true "" == false // true null == undefined // true // === (strict equality) — no coercion. ALWAYS USE THIS. 0 === "" // false 0 === false // false null === undefined // false

Use === always. Pretend == does not exist. This single rule eliminates an entire category of JavaScript bugs.

III. Strings and Template Literals

// Template literals (backticks) — JavaScript's f-strings const name = "World"; const greeting = `Hello, ${name}!`; const math = `2 + 2 = ${2 + 2}`; // Multi-line strings (backticks preserve newlines) const html = ` <div class="card"> <h2>${title}</h2> <p>${description}</p> </div> `; // Common string methods "Hello".toLowerCase() // "hello" "Hello".includes("ell") // true "Hello".startsWith("He") // true "Hello".slice(1, 3) // "el" "a,b,c".split(",") // ["a", "b", "c"] ["a", "b", "c"].join(", ") // "a, b, c" " hello ".trim() // "hello" "ha".repeat(3) // "hahaha" "42".padStart(5, "0") // "00042"

IV. Objects

Objects are JavaScript’s fundamental compound type. They are key-value maps where keys are strings (or Symbols) and values are anything.

// Object literal const user = { name: "Alice", age: 30, email: "alice@example.com", greet() { return `Hi, I'm ${this.name}`; } }; // Access user.name // "Alice" (dot notation) user["name"] // "Alice" (bracket notation — dynamic keys) // Destructuring (one of JS's best features) const { name, age } = user; // name = "Alice", age = 30 // Spread operator const updated = { ...user, age: 31 }; // shallow copy with age changed // Computed property names const key = "score"; const obj = { [key]: 100 }; // { score: 100 } // Shorthand (variable name matches key name) const x = 10, y = 20; const point = { x, y }; // { x: 10, y: 20 } // Useful Object methods Object.keys(user) // ["name", "age", "email", "greet"] Object.values(user) // ["Alice", 30, "alice@example.com", ƒ] Object.entries(user) // [["name","Alice"], ["age",30], ...]

V. Arrays

Arrays in JavaScript are objects with numeric keys and a length property. Their method toolkit is extraordinary.

const nums = [1, 2, 3, 4, 5]; // Transform: map (returns new array) const doubled = nums.map(n => n * 2); // [2, 4, 6, 8, 10] // Filter: keep elements matching a condition const evens = nums.filter(n => n % 2 === 0); // [2, 4] // Reduce: fold into a single value const sum = nums.reduce((acc, n) => acc + n, 0); // 15 // Find: first element matching a condition const found = nums.find(n => n > 3); // 4 // Test conditions nums.some(n => n > 4) // true (at least one) nums.every(n => n > 0) // true (all of them) nums.includes(3) // true // Flatten [[1, 2], [3, 4]].flat() // [1, 2, 3, 4] [[1, 2], [3, 4]].flatMap(a => a) // [1, 2, 3, 4] // Destructuring const [first, second, ...rest] = nums; // first = 1, second = 2, rest = [3, 4, 5] // Spread const combined = [...nums, 6, 7]; // [1, 2, 3, 4, 5, 6, 7] // Chaining: the real power of array methods const result = users .filter(u => u.age >= 18) .map(u => u.name) .sort() .join(", ");

Learn map, filter, and reduce. These three methods replace most for loops and produce cleaner, more declarative code. Chain them together and you can express complex data transformations in a single readable expression.

VI. Functions

// Function declaration (hoisted) function add(a, b) { return a + b; } // Function expression (not hoisted) const multiply = function(a, b) { return a * b; }; // Arrow function (the modern way) const square = (x) => x * x; const greet = (name) => `Hello, ${name}`; const add2 = (a, b) => a + b; // Default parameters function greet(name = "World") { return `Hello, ${name}`; } // Rest parameters function sum(...nums) { return nums.reduce((a, b) => a + b, 0); } sum(1, 2, 3, 4) // 10

Closures

A closure is a function that remembers the variables from the scope in which it was created, even after that scope has closed. This is JavaScript’s most powerful feature.

function createCounter(initial = 0) { let count = initial; // private variable — no one outside can touch it return { increment() { count++; }, decrement() { count--; }, getCount() { return count; } }; } const counter = createCounter(10); counter.increment(); counter.increment(); counter.getCount(); // 12 // count is truly private — there is no way to access it directly

Closures are how JavaScript achieves encapsulation without classes. The inner functions (increment, decrement, getCount) “close over” the count variable. They retain access to it even after createCounter has returned. React hooks, Redux reducers, Express middleware, and most JavaScript patterns are built on closures.

VII. Control Flow

// if / else if (x > 0) { console.log("positive"); } else if (x === 0) { console.log("zero"); } else { console.log("negative"); } // Ternary operator const label = age >= 18 ? "adult" : "minor"; // for...of (iterate over values — use this for arrays) for (const item of [10, 20, 30]) { console.log(item); } // for...in (iterate over keys — use for objects, not arrays) for (const key in user) { console.log(`${key}: ${user[key]}`); } // Classic for loop for (let i = 0; i < 10; i++) { console.log(i); } // Optional chaining (?.) — stops at null/undefined instead of crashing const city = user?.address?.city; // undefined if any link is null/undefined // Nullish coalescing (??) — default only for null/undefined (not "" or 0) const port = config.port ?? 3000;

Optional chaining (?.) and nullish coalescing (??) were added in ES2020 and immediately became essential. ?. prevents the “Cannot read property of undefined” error that plagues JavaScript code. ?? is a better || for defaults because it only triggers on null/undefined, not on 0, "", or false.

VIII. Prototypes and this

Every JavaScript object has a hidden link to another object called its prototype. When you access a property that does not exist on the object, JavaScript looks up the prototype chain until it finds it or reaches null.

const animal = { speak() { return `${this.name} makes a sound`; } }; const dog = Object.create(animal); dog.name = "Rex"; dog.speak() // "Rex makes a sound" — found speak() on the prototype // dog does not have its own speak(). JavaScript looked up the // prototype chain and found it on animal.

What this Actually Means

const obj = { name: "Alice", // Regular method: `this` is the object the method is called on greet() { return `Hi, I'm ${this.name}`; }, // Arrow function: `this` is inherited from the enclosing scope greetLater() { setTimeout(() => { console.log(`Hi, I'm ${this.name}`); // works! arrow inherits this }, 1000); } }; obj.greet(); // "Hi, I'm Alice" — this = obj const fn = obj.greet; fn(); // "Hi, I'm undefined" — this = global object (or undefined in strict mode)
ContextWhat this Is
Method call: obj.method()obj
Function call: fn()globalThis (or undefined in strict mode)
Arrow functionInherited from the enclosing lexical scope
Constructor: new Foo()The newly created object
fn.call(ctx) / fn.bind(ctx)ctx (explicitly set)

The rule is simple once you learn it: this is determined by how a function is called, not where it is defined. Arrow functions are the exception: they capture this from where they are defined. When in doubt, use arrow functions.

IX. Classes

ES6 classes are syntactic sugar over prototypes. They do not introduce a new object model; they make the existing one easier to use.

class Point { // Private field (ES2022) #label; constructor(x, y, label = "point") { this.x = x; this.y = y; this.#label = label; } distanceTo(other) { const dx = this.x - other.x; const dy = this.y - other.y; return Math.sqrt(dx * dx + dy * dy); } toString() { return `(${this.x}, ${this.y})`; } get magnitude() { return Math.sqrt(this.x ** 2 + this.y ** 2); } static origin() { return new Point(0, 0); } } const p = new Point(3, 4); p.magnitude // 5 (getter looks like a property) Point.origin() // Point { x: 0, y: 0 }

Inheritance

class Shape { constructor(name) { this.name = name; } area() { throw new Error("Not implemented"); } } class Circle extends Shape { constructor(radius) { super("circle"); this.radius = radius; } area() { return Math.PI * this.radius ** 2; } }

X. Promises and Async/Await

JavaScript is single-threaded. It handles concurrency through an event loop and asynchronous callbacks. Promises and async/await make asynchronous code readable.

Promises

// A Promise represents a value that may not be available yet const promise = new Promise((resolve, reject) => { setTimeout(() => resolve("done!"), 1000); }); promise .then(result => console.log(result)) // "done!" after 1 second .catch(err => console.log(err)) // handles errors .finally(() => console.log("cleanup")); // always runs // Promise.all: run multiple promises in parallel, wait for all const [a, b, c] = await Promise.all([ fetch("/api/users"), fetch("/api/posts"), fetch("/api/comments"), ]); // Promise.race: first one to settle wins const fastest = await Promise.race([ fetch("/api/primary"), fetch("/api/fallback"), ]);

Async/Await

// async functions return Promises automatically async function fetchUser(id) { const response = await fetch(`/api/users/${id}`); if (!response.ok) { throw new Error(`HTTP ${response.status}`); } return await response.json(); } // Error handling with try/catch async function main() { try { const user = await fetchUser(1); console.log(user.name); } catch (err) { console.log(`Failed: ${err.message}`); } }

async/await is the best syntax for asynchronous programming in any mainstream language. What was nested callbacks (“callback hell”) in 2012 is clean, linear, sequential-looking code today. Under the hood it is still Promises and the event loop. The syntax just makes it readable.

XI. The Event Loop

JavaScript runs on a single thread. There are no mutexes, no deadlocks, no race conditions on shared memory. Instead, JavaScript uses an event loop: a queue of tasks that are executed one at a time.

console.log("1"); setTimeout(() => console.log("2"), 0); // 0ms delay — but still queued Promise.resolve().then(() => console.log("3")); console.log("4");
1 4 3 2

The output order is: 1, 4, 3, 2. Why?

  1. "1" runs immediately (synchronous)
  2. setTimeout schedules "2" in the task queue (macrotask)
  3. Promise.then schedules "3" in the microtask queue
  4. "4" runs immediately (synchronous)
  5. The call stack is empty. Microtasks run first: "3"
  6. Then the next macrotask: "2"

The mental model: synchronous code runs first, microtasks (Promises) run next, macrotasks (setTimeout, I/O callbacks) run last. This is why a setTimeout(fn, 0) does not run immediately — it runs after all synchronous code and all microtasks have finished.

XII. Modules

// --- math.js --- export function add(a, b) { return a + b; } export function multiply(a, b) { return a * b; } export const PI = 3.14159; // Default export (one per module) export default class Calculator { /* ... */ } // --- main.js --- import Calculator from "./math.js"; // default import import { add, multiply, PI } from "./math.js"; // named imports import * as math from "./math.js"; // namespace import // Dynamic import (code splitting) const module = await import("./heavy-module.js");

XIII. Error Handling

try { const data = JSON.parse(rawInput); processData(data); } catch (err) { console.log(`Error: ${err.message}`); } finally { console.log("always runs"); } // Custom error classes class NotFoundError extends Error { constructor(resource, id) { super(`${resource} #${id} not found`); this.name = "NotFoundError"; this.resource = resource; this.id = id; } } // Always throw Error objects, never strings throw new Error("Something failed"); // good (has stack trace) throw "Something failed"; // bad (no stack trace)

XIV. The DOM

The DOM is the browser’s API for interacting with HTML. Even if you use React or Vue, they call these APIs under the hood.

// Select elements const el = document.querySelector(".my-class"); const all = document.querySelectorAll("ul > li"); // Create and modify const div = document.createElement("div"); div.textContent = "Hello"; div.className = "greeting"; document.body.appendChild(div); // Event handling const button = document.querySelector("#btn"); button.addEventListener("click", (event) => { console.log("clicked!"); }); // Event delegation: one listener on the parent, not one per child const list = document.querySelector("#list"); list.addEventListener("click", (e) => { if (e.target.tagName === "LI") { e.target.classList.toggle("selected"); } });

XV. The Ecosystem

// Initialize a project $ npm init -y // Install dependencies $ npm install express # production dependency $ npm install --save-dev jest # dev dependency

npm has over two million packages. The node_modules directory is infamous for its size. This is a real problem, though tools like pnpm mitigate it with hard links and deduplication.

TypeScript

TypeScript is JavaScript with static types. It compiles to plain JavaScript. It has become the default for serious projects.

function add(a: number, b: number): number { return a + b; } interface User { name: string; age: number; email?: string; // optional } function greet(user: User): string { return `Hello, ${user.name}`; }

TypeScript catches bugs at compile time that JavaScript catches at runtime (or never). Every major framework now recommends TypeScript. If you are writing JavaScript professionally, you should be writing TypeScript.

XVI. Putting It Together

A complete Node.js program demonstrating async/await, closures, array methods, destructuring, and error handling: a concurrent URL fetcher with rate limiting.

// fetcher.js — run with: node fetcher.js function createFetcher(maxConcurrent = 3) { let active = 0; const queue = []; async function execute(url) { while (active >= maxConcurrent) { await new Promise(r => queue.push(r)); } active++; try { const start = Date.now(); const res = await fetch(url); const text = await res.text(); return { url, status: res.status, size: text.length, elapsed: Date.now() - start, ok: res.ok }; } catch (err) { return { url, status: 0, size: 0, elapsed: 0, ok: false, error: err.message }; } finally { active--; if (queue.length) queue.shift()(); } } return { execute }; } async function main() { const urls = [ "https://jsonplaceholder.typicode.com/posts/1", "https://jsonplaceholder.typicode.com/posts/2", "https://jsonplaceholder.typicode.com/users/1", "https://this-will-fail.invalid/nope", ]; const fetcher = createFetcher(3); const results = await Promise.all(urls.map(u => fetcher.execute(u))); // Partition with reduce const { ok, failed } = results.reduce( (acc, r) => { (r.ok ? acc.ok : acc.failed).push(r); return acc; }, { ok: [], failed: [] } ); console.log("=== Successes ==="); ok.forEach(({ url, status, size, elapsed }) => console.log(` ${status} ${url} (${size}B, ${elapsed}ms)`) ); console.log("=== Failures ==="); failed.forEach(({ url, error }) => console.log(` FAIL ${url}: ${error ?? "HTTP error"}`) ); const avgTime = ok.reduce((s, r) => s + r.elapsed, 0) / ok.length; console.log(`\nSucceeded: ${ok.length}/${results.length}, avg ${avgTime.toFixed(0)}ms`); } main().catch(err => { console.error(err); process.exit(1); });

JavaScript’s Quirks

ExpressionResultWhy
[] + []""Both arrays coerce to empty strings, then concatenate
[] + {}"[object Object]"Array → "", Object → "[object Object]", then string concatenation
NaN !== NaNtrueIEEE 754 spec. Use Number.isNaN()
0.1 + 0.2 !== 0.3trueIEEE 754 floats. Result is 0.30000000000000004. Affects every language with floats.
typeof null"object"A bug from 1995. Never fixed for backwards compatibility.

These quirks are real, documented, and make for entertaining conference talks. They rarely matter in practice. Use ===, avoid implicit coercion, use Number.isNaN(), and you will never hit them in production.

Summary

JavaScript is not a perfect language. It was designed in ten days and carries the scars. But it has genuine strengths:

  • Ubiquity. It runs in every browser, on every server with Node.js, on mobile (React Native), on the desktop (Electron). No other language has this reach.
  • First-class functions and closures. Taken from Scheme, these are the foundation of everything good in JavaScript: callbacks, higher-order functions, module patterns, React components.
  • The event loop. A simple concurrency model. No threads, no mutexes, no deadlocks. Just callbacks and a queue.
  • Async/await. The cleanest syntax for asynchronous programming in any mainstream language.
  • The ecosystem. npm has more packages than any other registry. Whatever you need, someone has built a library for it.
  • TypeScript. The safety net JavaScript always needed. Static types without sacrificing flexibility.

JavaScript conquered the world not because it was the best language, but because it was there — in every browser, with no installation required. And then, year by year, it got better: let/const, arrow functions, destructuring, modules, async/await, optional chaining, private class fields. The JavaScript of today is dramatically better than the JavaScript of 2005.

If you understand closures, the prototype chain, the event loop, and Promises, you understand JavaScript. Everything else — React, Node, TypeScript, every framework of the year — is built on those foundations.