Skip to the notes
JSGroundwork
JSGroundwork handwritten · web dev
✎Playground→⌘Problems↻Review🔥Progress

Chapters

27 chapters
⌕
Beginner9›
B1Setup & mental modelB2Types & valuesB3Operators & flowB4Functions (first half)B5Objects & arrays (first half)B6DOM & eventsB7Basic asyncB8Errors & tools★Cheat page
Intermediate10›
I1Scope & functions, properlyI2Objects deeplyI3Prototypes & OOPI4Async, properlyI5Modules & toolingI6Regex, dates & APIsI7Error handlingI8Real-time connectionsI9Offline & storage★Cheat page
Advanced10›
A1Engine & memoryA2Advanced asyncA3MetaprogrammingA4Types & dataA5Patterns & architectureA6PerformanceA7SecurityA8EcosystemA9Testing★Cheat page
/ search[ ] chaptert top

JavaScript levels

1Beginner2Intermediate3Advanced

Ready to read

JSJavaScript⑂Git◎Interview prepΣDSA in JSSDSystem Design
More topics15›
</>HTML{ }CSS⚛ReactNNext.jsNeNest.jsTSTypeScriptNoNode.js🐳DockerDBSQL & Databases✓Testing🔒Web Security☁Cloud & DevOps◈GraphQL◆Redis☸Kubernetes
100%
I5

Modules & tooling

Everything that turns files full of JS into one thing a browser can run.

Nothing in this chapter runs in the sandbox above the way earlier .try blocks did — import/export are only valid inside a real module, not inside an arbitrary function body, so every example here is read, not clicked.

ESM — import and export

// math.js
export const PI = 3.14159;
export function square(n) { return n * n; }
export default function add(a, b) { return a + b; }   // at most ONE default per module

// app.js
import add, { PI, square } from "./math.js";   // default + named, one import statement
import * as math from "./math.js";              // everything, under one namespace object
Rule An imported binding is a live view into the exporting module, not a value copied once at import time. If math.js later reassigns an exported let, every file that imported it sees the new value — the same "reference, not snapshot" idea from objects and arrays, applied to module bindings instead of object properties.

This live-binding rule, plus imports being static — resolved before any module code runs, always at the top level, never conditional — is exactly what lets a bundler safely tree-shake: it can see every import/export at compile time and delete anything nothing else actually uses, something CommonJS's fully dynamic require() can't guarantee.

CommonJS vs ESM

CommonJS (Node's original) ESM
Syntaxrequire() / module.exportsimport / export
Loadingsynchronouscan be async (dynamic import())
Resolvedat runtime, can be conditionalstatically, before execution
Top-level thismodule.exportsundefined
File markers.cjs, or default in a plain package.json.mjs, or "type": "module" in package.json

Node runs both today; browsers only ever understood ESM (<script type="module">). ESM is the forward direction — new libraries default to it, and most tooling exists partly to smooth over the gap for code still shipping CommonJS.

Dynamic import()

button.addEventListener("click", async () => {
  const { openModal } = await import("./modal.js");   // only fetched when actually needed
  openModal();
});

Unlike a static import, this one is a real function call — it can go inside an if, a click handler, anywhere — and it returns a promise. This is the mechanism behind code splitting: a bundler sees a dynamic import() and automatically cuts that module (and everything only it needs) into its own separate file, downloaded only when that line actually runs, instead of bloating the very first page load with code most visitors may never trigger.

npm, package.json, semver

{
  "name": "my-app",
  "version": "1.4.2",
  "dependencies": { "react": "^18.2.0" },
  "devDependencies": { "vitest": "^4.1.10" }
}

A version is MAJOR.MINOR.PATCH — major for breaking changes, minor for new, backward-compatible features, patch for backward-compatible fixes. The prefix in front of a dependency's version controls how far an install is allowed to drift:

Range Allows
^18.2.0anything up to, not including, 19.0.0 — new minors and patches, never a new major
~18.2.0anything up to, not including, 18.3.0 — patches only
18.2.0that exact version, nothing else
⚠ package.json alone isn't reproducible ^18.2.0 is a range, not one specific version — two installs weeks apart can legitimately resolve to different actual versions. package-lock.json (or yarn.lock, pnpm-lock.yaml) pins the exact resolved tree, which is why it's committed to the repo and why "works on my machine" so often traces back to a missing or ignored lockfile.

A bundler, briefly

A bundler (Vite, webpack, esbuild, Rollup) does three jobs at once: follows every import to build one dependency graph, transpiles newer syntax and JSX/TS down to something the target browsers understand, and packs the result into as few files as make sense (splitting where a dynamic import() says to). The source map it emits alongside the bundle is what lets a browser's DevTools show your original Button.tsx and its real line numbers in a stack trace, instead of line 1 of one giant minified file.

Linting and formatting

Two different jobs, often confused because they're configured together. ESLint reads your code for actual problems — an unused variable, a missing dependency in a React hook, a variable that shadows an outer one by accident. Prettier doesn't look for problems at all — it just rewrites every file into one consistent style (quotes, spacing, line length), so a diff shows what actually changed instead of a formatting argument. Running both: Prettier decides how the code looks, ESLint decides whether the code is right.

Practice this layer

Opens in the editor — write it, run it, and check it against real tests.

Does this version satisfy a caret range?5 tests · intermediateBuild a left-to-right pipe3 tests · intermediate
←previousAsync, properly↑ CovernextRegex, dates & APIs→