How Lodash replace Handles Regex Iteration

Lodash’s _.replace provides a functional wrapper around JavaScript’s native string replacement algorithm, delegating iterative matching to ECMAScript regex execution protocols. This article examines the evaluation techniques governing these iterative loops, detailing how Lodash normalizes inputs, how the engine traverses the target string using the RegExp.prototype[Symbol.replace] protocol, and how internal descriptors and state registers process match iterations.

Lodash Wrapper and Spec Delegation

In Lodash, _.replace(string, pattern, replacement) functions as a thin coercion and delegation boundary. The library normalizes the target input using an internal toString method, then natively invokes String.prototype.replace. Because Lodash delegates evaluation directly to the host environment, regex iterative mechanics are governed by the ECMAScript specification for RegExp.prototype[Symbol.replace].

If pattern is a standard string, the operation performs a single literal search and terminates. When pattern is a RegExp object, evaluation transfers entirely to the engine's native regular expression engine (such as V8's Irregexp in Node.js and Chromium).

The Global Iteration Loop and lastIndex Mechanics

Iterative evaluation is triggered exclusively when the regex pattern includes the global (g) or sticky (y) flags. Without these flags, no iteration occurs; the engine identifies the first match and immediately resolves the replacement.

When the g flag is enabled, the execution enters an internal looping descriptor governed by the following sequence:

  1. State Reset: The engine initializes the search index, setting the regex instance's lastIndex property to 0.
  2. Sequential RegExpExec Invocation: Inside a continuous loop, the engine repeatedly invokes the abstract specification operation RegExpExec(R, S) on the string.
  3. Array Match Construction: Each successful match yields a result array containing the matched string, any parenthesized capture groups, the starting index of the match, and the input string.
  4. Pointer Mutation: The lastIndex pointer automatically updates to the position immediately following the end of the last matched substring.

Zero-Width Match Handling and Loop Advancement

A critical edge case in iterative evaluation occurs when a regular expression matches an empty string (e.g., /^/g, /(?:)/g, or /a*/g on non-matching characters). If lastIndex does not advance, an infinite loop ensues.

To prevent infinite loops during iterative replacement, the engine implements the specification's AdvanceStringIndex routine:

Parameter Mapping and Replacer Descriptors

Once the iterative loop resolves the matched spans, it binds match metadata to iterative descriptor arguments, depending on whether replacement is a function or a replacement pattern string.

Functional Descriptors

When replacement is provided as an invocable function, the engine dynamically unpacks match tokens per iteration into the function's arguments:

The return value of this descriptor replaces the slice defined from the match's start index to its end index.

Substitution Tokens

When replacement is a string, the evaluation loop scans the string for $ substitution tokens:

Memory Optimization and Chunked Concatenation

During the iterative loop, modern JavaScript engines do not mutate the original string in place. Instead, the evaluation loop maintains an internal list of string segments:

  1. The preceding un-matched slice of the input string.
  2. The evaluated replacement descriptor result.
  3. Subsequent unmodified slices between matched offsets.

When the iteration loop finishes upon receiving a null match descriptor, the engine computes the total required allocation and joins the accumulated segments into a new immutable string primitive, which Lodash returns to the caller.