In JavaScript's String.prototype.replace, what arguments does a replacer function receive, and when would you use one instead of a replacement string containing $& or $1?
answer
- match first, input last
- one argument per capture group
- offset sits between them
- the return value is used verbatim
- no dollar expansion in the function form
basics
~20 sA replacer function is called once per match with the whole matched text, then each capture group, then the match offset and the full input string (plus a groups object when the pattern has named captures). Its return value is inserted verbatim, with no dollar-sign expansion.
solid answer
~50 sThe replacer receives, in order: the full matched substring, then one argument per capture group, then the zero-based `offset` of the match, then the whole input string — and, if the pattern declares named captures, a final object of them. It runs once per replacement, left to right, and whatever it returns is coerced to a string and inserted **literally**. Reach for it whenever the replacement has to be *computed* rather than assembled: uppercasing what matched, looking a token up in a map, formatting a number, or deciding conditionally to leave the match alone by returning the match itself. The other big reason is safety — a replacement *string* expands `$$`, `$&`, `` $` ``, `$'`, `$1` and `$<name>`, so any `$` in data can splice unintended text into the output. A function's return value gets none of that treatment, which makes it the correct way to insert untrusted text.
code
javascript · 15 linesconst vars = { name: 'Ada', role: 'engineer' };
const out = 'Hi {name}, the {role} — {missing}'.replace(
/\{(\w+)\}/g,
(match, key, offset, input) => {
console.log(offset, input.length);
return key in vars ? vars[key] : match; // leave unknown keys alone
},
);
console.log(out); // 'Hi Ada, the engineer — {missing}'
// Return values are inserted verbatim — no $ expansion:
console.log('x'.replace(/x/, () => '$&')); // '$&'
console.log('x'.replace(/x/, '$&')); // 'x'go deeper
Know that the second argument to replace can be a function, that it is called once per match, and that its first argument is the text that matched.
State the full argument order — match, each capture, offset, input, then a named-groups object when present — and explain that the return value is inserted verbatim with no dollar-sign expansion.
Choose the function form deliberately for computed, conditional or position-aware replacements, and treat it as the safe channel for inserting text you did not author.
Set the convention that any replacement derived from data goes through a replacer function rather than a template string, so the $-expansion trap cannot appear in the codebase at all.
## The two kinds of replacement `String.prototype.replace` and `replaceAll` accept either a **string** or a **function** as the second argument. They are not interchangeable: the string form is a mini-template language, the function form is arbitrary computation. ## The string form is a template, not a literal In a replacement string these sequences are expanded: | Sequence | Inserts | |---|---| | `$$` | a literal `$` | | `$&` | the matched substring | | `` $` `` | the portion of the input before the match | | `$'` | the portion of the input after the match | | `$1` … `$99` | the numbered capture group | | `$<name>` | the named capture group | Anything else beginning with `$` is left alone. This is convenient for simple rewrites — `'2026-08-19'.replace(/(\d+)-(\d+)-(\d+)/, '$3/$2/$1')` gives `'19/08/2026'` — and dangerous when the replacement text is data: ```js const userName = "$&$&"; 'hello NAME'.replace('NAME', userName); // 'hello NAMENAME' ``` ## The function form and its argument list When the replacement is a function, the engine calls it once per match with a positional argument list: ```js 'a1 b2'.replace(/([a-z])(\d)/g, (match, letter, digit, offset, input) => { return `${letter.toUpperCase()}${Number(digit) * 2}`; }); // 'A2 B4' ``` The order is fixed: `match`, then `p1 … pn` (one per capture group in the pattern, `undefined` for a group that did not participate), then `offset`, then the whole `input` string. When the pattern declares named capture groups, one further argument follows — an object mapping those names to their captured text. Because the count of capture arguments varies with the pattern, a generic helper collects them with a rest parameter and pulls the fixed ones off the end. The function's return value is converted with `String()` and inserted **exactly as returned**. No `$` expansion happens. Returning `undefined` inserts the string `'undefined'` — a common bug in a replacer with a conditional branch that forgets to return the original match. ## When the function form is the right call **Computation.** Anything the template language cannot express: arithmetic, case conversion, date formatting, escaping. ```js const escapeHtml = (s) => s.replace(/[&<>"']/g, (c) => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''', })[c]); ``` **Lookup.** Template interpolation against a data object, where the replacement depends on the captured key. **Conditional replacement.** Return `match` unchanged to leave that occurrence alone — the only way to skip individual matches, since a pattern either matches or does not: ```js text.replace(/\b\d+\b/g, (m) => (Number(m) > 100 ? 'BIG' : m)); ``` **Position-aware output.** The `offset` argument lets you treat the first match differently, or record where substitutions happened. **Safety.** Inserting text you did not author. A function's return value is never re-scanned for `$` patterns, so no injection through the replacement channel is possible. If you must use the string form with untrusted text, double every dollar first: `value.replaceAll('$', '$$$$')` — four dollars in the source because that replacement string is itself template-expanded. **Side effects.** Because it runs once per match, a replacer can also count or collect matches while it rewrites, though `matchAll` is the cleaner tool when rewriting is not actually needed. ## Ordering and call count With a global regex, the replacer is invoked once per non-overlapping match, in left-to-right order, and each call sees the **original** input string as its last argument — not a partially rewritten one. Replacements never feed back into the matching, so a replacer that emits text matching the pattern will not cause a second pass. Without the `g` flag it is invoked at most once. If there is no match, it is never invoked at all, and `replace` returns the input string unchanged. ## The usual mistakes Returning nothing from one branch (yields `'undefined'`); assuming the last argument is always the input string when the pattern has named groups (it is the groups object then); assuming captures are passed as an array rather than as separate positional arguments; and passing a *call* rather than the function itself — `replace(re, fn())` invokes `fn` once, immediately, and hands `replace` its return value.
- How do you write a replacer that works for a pattern with an unknown number of capture groups?Collect everything with a rest parameter and take the fixed arguments off the end: `(match, ...rest) => { const input = rest.pop(); const offset = rest.pop(); /* rest is now the captures */ }`. Watch out for named groups — when the pattern has them, an extra groups object sits after the input string, so pop that first.
- What does a replacer function return to leave a particular match untouched?The match itself — the function's first argument. `text.replace(re, (m) => shouldSkip(m) ? m : rewrite(m))`. There is no other mechanism for skipping an individual occurrence, because the regex either matched or it did not. Returning `undefined` does not skip it; it inserts the literal text `'undefined'`.
- Does the input string argument reflect earlier replacements when the regex is global?No. Every invocation receives the original, unmodified input, and the `offset` is an index into that original. Replacements are assembled into a new string as the engine goes, and emitted text is never re-scanned, so a replacer that outputs something matching the pattern will not trigger another substitution.
saying these in an interview costs you the question
- Thinks the capture groups arrive as one array argument
- Forgets to return the match in a conditional branch
- Assumes the last argument is always the input string
- Believes $& is expanded in a function's return value
- Passes fn() instead of fn as the replacement