Methods of RegExp and String

A regular expression is only half the story. The other half is the method you hand it to. Some methods live on strings (str.match, str.replace), some live on the regexp itself (regexp.exec, regexp.test), and each has its own rules about the pattern:g flag, capturing groups, and what it returns when nothing matches. Pick the wrong one and you get null where you expected an array, or a stale search position that quietly breaks your second call.

This article walks through all of them. Keep one distinction in the back of your mind the whole way: string methods forget everything between calls, regexp methods can remember — the ones called on a regexp track a search position in regexp.lastIndex, and that memory is the source of most surprises.

methodcalled onreturns
matchstringarray or null
matchAllstringiterable of arrays
splitstringarray of pieces
searchstringindex or -1
replacestringnew string
replaceAllstringnew string
execregexparray or null (tracks lastIndex)
testregexptrue/false (tracks lastIndex)
Where each method lives and what it hands back.

str.match(regexp)

str.match(regexp) searches str for regexp. What it hands back depends entirely on whether the pattern:g flag is present — there are three outcomes.

1. No pattern:g flag: the first match, with detail. You get an array-like result. Slot 0 is the full match, slots 1, 2, … are the capturing groups, and two extra properties ride along: index (where the match starts) and input (the original string).

let str = "Meet the Superhero";

let result = str.match(/Super(hero)/);

alert( result[0] );     // Superhero (full match)
alert( result[1] );     // hero (first capturing group)
alert( result.length ); // 2

// Additional information:
alert( result.index );  // 9 (match position)
alert( result.input );  // Meet the Superhero (source string)
[0]
“Superhero”
[1]
“hero”
.index
9
.input
“Meet the Superhero”
Without the g flag, match returns one rich result: full match at [0], groups after it, plus index and input.

2. With pattern:g: every match, as plain strings. The result is a flat array of matched substrings. No groups, no index, no input — those details are dropped in favor of listing everything the pattern found.

let str = "Meet the Superhero";

let result = str.match(/Super(hero)/g);

alert( result[0] ); // Superhero
alert( result.length ); // 1

3. No matches: null, not an empty array. This trips people up constantly. A miss returns null in both modes, so any code that assumes an array will throw the moment the string doesn’t match.

let str = "Meet the Superhero";

let result = str.match(/Villain/);

alert(result); // null
alert(result.length); // Error: Cannot read property 'length' of null

The tidy fix is a fallback with ||, so the variable is always an array:

let result = str.match(regexp) || [];

Try it yourself. Toggle the pattern:g flag on and off and watch the result switch between one rich array (with .index and .input) and a flat list of strings — or type a pattern that misses to see the bare null:

interactivestr.match: g flag vs no flag vs no match

str.matchAll(regexp)

str.matchAll(regexp) is the modern answer to “give me every match and every group”. Think of it as match with the pattern:g flag but without throwing away the group details. Three things set it apart from match:

  1. It returns an iterable of results, not a plain array. Run it through Array.from (or spread) if you want indexing.
  2. Each result is a full array with capturing groups, same shape as match without pattern:g — including index and input.
  3. No matches gives an empty iterable, never null. No guard needed.
let str = '<p>Draft saved</p>';
let regexp = /<(.*?)>/g;

let matchAll = str.matchAll(regexp);

alert(matchAll); // [object RegExp String Iterator], not array, but an iterable

matchAll = Array.from(matchAll); // array now

let firstMatch = matchAll[0];
alert( firstMatch[0] );  // <p>
alert( firstMatch[1] );  // p
alert( firstMatch.index );  // 0
alert( firstMatch.input );  // <p>Draft saved</p>

Because the return value is lazy and iterable, for..of is often the cleanest way to consume it — no Array.from needed, and results are computed one at a time as you loop:

let str = '<p>Draft saved</p>';

for (let match of str.matchAll(/<(.*?)>/g)) {
  alert(`${match[0]} (tag: ${match[1]})`);
  // <p> (tag: p), then </p> (tag: /p)
}
match
no g
1 match + groups
match
with g
all matches, no groups
matchAll
g required
all matches + groups
Picking a matching method: how much do you need, and how many results?

str.split(regexp | substr, limit)

split chops a string into an array of pieces, cutting at each delimiter. The delimiter can be a plain string:

alert('88-52-14'.split('-')) // array of ['88', '52', '14']

Or a regexp, which lets the separator itself vary. Here commas may or may not be followed by spaces, and pattern:\s* absorbs whichever spacing appears:

alert('88, 52, 14'.split(/,\s*/)) // array of ['88', '52', '14']

The optional second argument, limit, caps how many pieces you get back. 'r-g-b-a'.split('-', 2) returns ['r', 'g'] and discards the rest.

See it split live. Change the separator pattern — try ,\s*, then \s*-\s*, then a capturing one like (,) — and watch the pieces (empty pieces show as ∅):

interactivestr.split with a regexp separator

str.search(regexp)

str.search(regexp) reports where the first match starts, or -1 when there’s no match. The pattern:i flag here makes the search case-insensitive.

let str = "The North wind blows cold";

alert( str.search( /north/i ) ); // 4 (first match position)

The limitation to remember: search only ever finds the first match. Even with the pattern:g flag it returns a single index. When you need the positions of later matches too, reach for str.matchAll(regexp) and read .index off each result.

str.replace(str | regexp, str | func)

This is the workhorse for search-and-replace. It accepts a string or a regexp to find, and a string or a function to substitute in.

At its simplest, replace one substring with another:

// replace a dash by a colon
alert('88-52-14'.replace("-", ":")) // 88:52-14

Notice the output: 88:52-14. Only the first dash changed.

Swap the string "-" for the regexp pattern:/-/g and the pattern:g flag does the rest:

// replace all dashes by a colon
alert( '88-52-14'.replace( /-/g, ":" ) )  // 88:52:14

Special patterns in the replacement string

The replacement string isn’t purely literal. A handful of $-sequences let you reference parts of the match:

Symbols Action in the replacement string
$& inserts the whole match
$` inserts the part of the string before the match
$' inserts the part of the string after the match
$n if n is a 1-2 digit number, inserts the contents of the n-th capturing group, see Capturing groups
$<name> inserts the contents of the group with the given name, see Capturing groups
$$ inserts a literal $ character

A classic use: reorder captured pieces. Two groups become $1 and $2, and we print them in reverse:

let str = "Maya Vance";

// swap first and last name
alert(str.replace(/(maya) (vance)/i, '$2, $1')) // Vance, Maya

A function as the replacement

When a fixed template isn’t enough, pass a function as the second argument. It runs once per match, and whatever it returns becomes the replacement text for that match. This is where replace gets its real power — you can compute the substitution from the match itself.

The function receives func(match, p1, p2, ..., pn, offset, input, groups):

  1. match — the full matched text,
  2. p1, p2, ..., pn — the captured groups, one argument each,
  3. offset — where the match starts in the string,
  4. input — the whole source string,
  5. groups — an object of named groups (present only if the pattern has named groups).

With no capturing parentheses in the pattern, the signature collapses to just func(match, offset, input).

match
“Maya Vance”
p1
“Maya”
p2
“Vance”
offset
0
input
“Maya Vance”
What each argument of the replacer function holds, for /(\\w+) (\\w+)/ matching 'Maya Vance'.

Uppercase every match by returning its uppercased form:

let str = "json or yaml";

let result = str.replace(/json|yaml/gi, str => str.toUpperCase());

alert(result); // JSON or YAML

Replace each match with its own position, using the offset argument:

alert("Go-Go-go".replace(/go/gi, (match, offset) => offset)); // 0-3-6

Here the replacer computes each substitution from the match itself: every run of digits is replaced by double its value. Edit the text, or change * 2 inside the function to * 10 to see the replacement recomputed per match:

interactiveA function replacer: double every number

Two capturing groups means the function is called with five arguments — full match, the two groups, then position and source (unused here):

let str = "Maya Vance";

let result = str.replace(/(\w+) (\w+)/, (match, name, surname) => `${surname}, ${name}`);

alert(result); // Vance, Maya

When there are many groups, rest parameters collect them into a single array. Note the indices: match[0] is the full match, so the groups start at match[1]:

let str = "Maya Vance";

let result = str.replace(/(\w+) (\w+)/, (...match) => `${match[2]}, ${match[1]}`);

alert(result); // Vance, Maya

With named groups, the groups object is always the last argument. Grab it by popping the end of the collected arguments:

let str = "Maya Vance";

let result = str.replace(/(?<name>\w+) (?<surname>\w+)/, (...match) => {
  let groups = match.pop();

  return `${groups.surname}, ${groups.name}`;
});

alert(result); // Vance, Maya

A function replacement sees everything about each match and can reach outer variables, so there is no reordering or transformation it can’t express. When a $-template feels cramped, switch to a function.

str.replaceAll(str | regexp, str | func)

replaceAll is replace with the first-match limitation lifted. Two differences matter:

  1. A string first argument replaces every occurrence, where replace would stop at the first.
  2. A regexp first argument must carry the pattern:g flag, or replaceAll throws. With pattern:g it behaves exactly like replace.

Its reason to exist is the string case — replacing all copies of a literal without building a regexp:

// replace all dashes by a colon
alert('88-52-14'.replaceAll("-", ":")) // 88:52:14

regexp.exec(str)

Now we cross over to methods called on the regexp itself. regexp.exec(str) returns a match, and its behavior forks on the pattern:g (or pattern:y) flag.

Without pattern:g, regexp.exec(str) returns the first match in exactly the same shape as str.match(regexp) — full match, groups, index, input. Nothing new there.

With pattern:g, it becomes stateful and that changes everything:

  • Each call returns the next match and records the position right after it in regexp.lastIndex.
  • The following call resumes the search from regexp.lastIndex.
  • When no more matches remain, exec returns null and resets regexp.lastIndex back to 0.

So repeated calls walk through every match in sequence, with lastIndex acting as a bookmark.

call 1
match at 8
lastIndex → 14
call 2
match at 26
lastIndex → 32
call 3
null
lastIndex → 0
A global regexp.exec advances lastIndex after each call, then returns null and resets to 0.

Before str.matchAll existed, the idiom for “all matches with their groups” was an exec loop, assigning inside the while condition so the loop ends when exec returns null:

let str = 'More on Nebula at https://nebula.space';
let regexp = /nebula/ig;

let result;

while (result = regexp.exec(str)) {
  alert( `Found ${result[0]} at position ${result.index}` );
  // Found Nebula at position 8, then
  // Found nebula at position 26
}

The loop still works, but str.matchAll is usually the cleaner choice today.

Because lastIndex is writable, you can start exec from any position by setting it yourself:

let str = 'Ready, steady, go';

let regexp = /\w+/g; // without flag "g", lastIndex property is ignored
regexp.lastIndex = 5; // search from 5th position (from the comma)

alert( regexp.exec(str) ); // steady

The sticky flag: pattern:y

The pattern:y (sticky) flag tightens this behavior: the match must begin exactly at regexp.lastIndex, not somewhere after it. Swap pattern:g for pattern:y in the example above and there is no word at position 5 (that’s the comma), so you get null:

let str = 'Ready, steady, go';

let regexp = /\w+/y;
regexp.lastIndex = 5; // search exactly at position 5

alert( regexp.exec(str) ); // null
R e a d y ,  s t e a d y
position 5 points at the comma ↑
flag g
skips ahead → matches “steady”
flag y
must match at 5 → null
At lastIndex 5, g scans forward to find a word; y demands a match right at 5 and fails.

The sticky flag shines when you’re tokenizing — reading tokens one after another at an exact cursor position, with no accidental skipping over unexpected characters.

regexp.test(str)

regexp.test(str) asks a yes/no question: is there a match anywhere? It returns true or false.

let str = "I admire Origami";

// these two tests do the same
alert( /admire/i.test(str) ); // true
alert( str.search(/admire/i) != -1 ); // true

A miss returns false:

let str = "Just some noise";

alert( /admire/i.test(str) ); // false
alert( str.search(/admire/i) != -1 ); // false

Here’s the catch: with the pattern:g flag, test is stateful too. Just like exec, it reads from regexp.lastIndex and advances it. So you can start a test from a chosen position:

let regexp = /admire/gi;

let str = "I admire Origami";

// start the search from position 10:
regexp.lastIndex = 10;
alert( regexp.test(str) ); // false (no match after position 10)