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%
A4

Types & data

What happens once a number, a string, or a file gets big or exotic enough to need its own type.

BigInt — exact integers, past 2^53

Every regular JS number is a 64-bit float, which means integers stop being exactly representable past Number.MAX_SAFE_INTEGER — 2^53 - 1. BigInt is a genuinely separate type for arbitrary-size integers with no such ceiling, spelled with a trailing n.

console.log(Number.MAX_SAFE_INTEGER + 1 === Number.MAX_SAFE_INTEGER + 2);   // what happens?
console.log(9007199254740991n + 1n === 9007199254740991n + 2n);              // same numbers, as BigInt — what happens?

true, then false. Past the safe integer limit, regular numbers genuinely can't tell MAX_SAFE_INTEGER + 1 and + 2 apart — both round to the same closest representable float. The n suffix versions stay exact, because BigInt isn't a float at all.

⚠ BigInt and Number don't mix 10n + 5 throws TypeError: Cannot mix BigInt and other types — there's no implicit conversion between them in either direction. Convert explicitly, one way or the other: 10n + BigInt(5) or Number(10n) + 5.

ArrayBuffer, TypedArrays, DataView

An ArrayBuffer is a fixed-length block of raw bytes — nothing more, no way to read or write it directly. A TypedArray (Int32Array, Uint8Array, etc.) is a typed view onto that same memory, interpreting its bytes as a specific numeric type. Multiple views can share one buffer at once, and writing through any of them changes the same underlying bytes every other view sees.

const buffer = new ArrayBuffer(4);       // 4 raw bytes
const asInt32 = new Int32Array(buffer);  // one view: "these 4 bytes are one 32-bit int"
asInt32[0] = 42;

const dv = new DataView(buffer);          // a second, more manual view of the SAME bytes
console.log(dv.getInt32(0));              // what happens?
console.log(dv.getInt32(0, true));        // second argument: littleEndian — what happens?

704643072, then 42. Not a bug — a genuine byte-order mismatch. TypedArrays use the platform's native byte order (little-endian on essentially every real device today). DataView.getInt32 defaults to big-endian unless you explicitly pass true for its second argument. Same 4 bytes, same buffer, two different interpretations of what order they represent a number in — exactly the kind of detail that matters the moment you're parsing a binary file format or a network protocol that specifies its own byte order.

Blob, File, FileReader

const blob = new Blob(["hello world"], { type: "text/plain" });
blob.size;    // 11 — bytes, not characters (matters once text isn't plain ASCII)
blob.type;    // "text/plain"

// A File (from an <input type="file"> or a drop event) is a Blob with a name and a modified date
fileInput.addEventListener("change", (e) => {
  const file = e.target.files[0];
  const reader = new FileReader();
  reader.onload = () => console.log(reader.result);   // the fully-read contents
  reader.readAsText(file);          // or readAsArrayBuffer, readAsDataURL
});

// modern alternative — same result, promise-based, no event wiring
const text = await file.text();
const bytes = await file.arrayBuffer();

FileReader predates promises; a File object itself now has .text()/.arrayBuffer() methods that return promises directly — same underlying read, no callback wiring needed in new code.

Unicode — code points vs code units

A JS string's .length counts UTF-16 code units, not visible characters. Most characters fit in one 16-bit unit; a large chunk of emoji and some rarer scripts need two units — a surrogate pair — and .length counts both of them as 2.

const emoji = "😀";
console.log(emoji.length);            // what happens?
console.log([...emoji].length);       // spreading iterates by CODE POINT, not code unit — what happens?
console.log(JSON.stringify(emoji[0])); // indexing still grabs one code UNIT — what happens?

2, then 1, then "\ud83d" — half of a surrogate pair, not a valid character on its own. emoji[0] silently cuts an emoji in half; spreading a string (or for...of, or Array.from) walks it by actual code point and never splits one. Slicing a string by raw index — a search-result excerpt, a truncated preview — risks exactly this cut, and it's an easy one to never notice until a specific emoji or script breaks in production.

"café".normalize("NFC").length === "café".normalize("NFC").length;
// true — but two strings that VISUALLY look identical can be genuinely unequal:
// "é" can be one single code point, OR "e" + a separate combining accent mark.
// .normalize() converts both spellings to one canonical form before comparing.
Rule Comparing user-typed text for equality without .normalize() first is a real, if rare, bug — two strings can render pixel-identical and still fail ===, if one came from a source that encodes accents differently.

Intl — past basic formatting

["café", "cafe", "cafz"].sort(new Intl.Collator("en").compare);
// ["cafe", "café", "cafz"] — locale-aware ordering; a plain .sort() compares raw code
// points instead, which gets accented characters and non-Latin scripts sorted wrong

new Intl.RelativeTimeFormat("en").format(-1, "day");   // "1 day ago"
new Intl.RelativeTimeFormat("en").format(3, "hour");   // "in 3 hours"

All three Intl constructors from this and earlier chapters — Collator, DateTimeFormat, NumberFormat, and RelativeTimeFormat — take the same first argument, a locale string, and are the built-in answer to "format this correctly for the reader's language and region" without hand-writing rules that differ by country.

Practice this layer

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

A factorial that doesn't lose precision4 tests · advancedCount actual characters, not code units4 tests · advanced
←previousMetaprogramming↑ CovernextPatterns & architecture→