Lesson 50 of 80
padStart, matchAll, replaceAll, and normalize quietly fixed four separate string problems people used to reach for regex or a loop to solve.
12 min read
Every option explains itself — why the right one is right, and why each wrong one is wrong. Miss one and it comes back later on a spaced-repetition schedule.
This topic is about judgment and practical tradeoffs more than a single right answer — a mock interview is the better way to test yourself on it.
Be interviewed on this →Hints without spoilers, and a plain explanation of why a wrong answer was wrong — answered only from this lesson. Free account, no card.
Sign up freeWhat interviewers actually ask about this, and what a strong answer has to cover.
A strong answer covers
Likely follow-ups
Strings got a handful of methods added over several ES versions that each replace a specific piece of boilerplate people wrote by hand for years. None of them are complicated on their own — the value is knowing they exist, so you stop reinventing them:
padStart / padEnd — pad a string to a target length with a filler character.replaceAll — replace every occurrence of a substring, not just the first.matchAll — get every regex match with capture groups, not just the matched text.normalize — collapse visually-identical Unicode strings into one canonical form so === actually works on them.Padding shows up constantly in anything tabular or ID-like:
function formatInvoiceId(n) {
return n.toString().padStart(6, "0"); // 42 -> "000042"
}
function formatLine(label, amount) {
const price = `$${amount.toFixed(2)}`;
return label.padEnd(20, ".") + price.padStart(10, " ");
// "Widget.............. $9.99"
}
replaceAll used to require a global regex (str.replace(/x/g, y)) just to hit every match — now a plain string works directly:
const path = "C:\\Users\\dev\\praxis";
path.replaceAll("\\", "/"); // "C:/Users/dev/praxis" — every backslash, not just the first
matchAll is the one people most often reach for .match() on and get burned — .match() with a global flag throws away capture groups. matchAll returns an iterator of full match objects, groups intact:
const log = "user:42 action:login user:7 action:logout";
const pairs = [...log.matchAll(/(\w+):(\w+)/g)];
pairs.map((m) => ({ key: m[1], value: m[2] }));
// [{ key: "user", value: "42" }, { key: "action", value: "login" }, ...]
And normalize matters the moment you compare or search user-entered Unicode — an accented character can be encoded two different ways (one precomposed codepoint, or a base letter plus a combining accent) and look identical while failing ===:
const a = "café"; // é as one codepoint (U+00E9)
const b = "cafe\u0301"; // e + combining acute accent (U+0301)
a === b; // false — different byte sequences, same rendering
a.normalize() === b.normalize(); // true — both collapse to the same canonical form
replaceAll with a string arg replaces literal occurrences, not a pattern. "a.b.c".replaceAll(".", "-") replaces every literal dot — no regex escaping needed. Pass a regex instead and it must have the g flag, or it throws.matchAll returns an iterator, not an array. You almost always want [...str.matchAll(re)] to actually use it with .map/.filter — the raw iterator only supports one pass.normalize() doesn't fix every Unicode comparison problem. It fixes canonically-equivalent forms (the café example), but visually-similar-but-different characters — a Cyrillic "а" next to a Latin "a" — normalize to themselves and still won't be equal. That's a different problem (homoglyph detection), not something normalize solves.replaceAll is doing for strings what array methods already did for lists — "act on every match, not just the first" is the same idea filter applies across a whole array in one pass. And matchAll is the string-specific case of the iterator protocol: it returns something built to be spread or looped with for...of, the same contract generators and other iterables follow.