How can you match or replace all occurrences in a string?
Let’s learn how to match or replace all occurrences of a pattern in a string.
You have previously learned about the replace()
and match()
methods, as well as the global g
modifier. Now you can combine that knowledge to handle all patterns in a string. Let’s recall our original match code:
const regex = /freeCodeCamp/;
const match = "freeCodeCamp".match(regex);
console.log(match);
And our resulting match object:
// [
// 'freeCodeCamp',
// index: 0,
// input: 'freeCodeCamp',
// groups: undefined
// ]
But what if we have a string with multiple occurrences of freecodecamp
to match? Let’s take a look at how match()
behaves with that. We’ll throw in our old replace()
example too, just to compare:
const regex = /freecodecamp/;
const str = "freecodecamp is the best we love freecodecamp";
const matched = str.match(regex);
const replaced = str.replace(regex, "freeCodeCamp");
console.log(matched);
console.log(replaced);
And the result is this:
// [
// 'freecodecamp',
// index: 0,
// input: 'freecodecamp is the best we love freecodecamp',
// groups: undefined
// ]
// freeCodeCamp is the best we love freecodecamp
Oh no! match()
only returned the first match, and replace()
only replaced the first match. This is because, by default, match()
and replace()
only operate against the first pattern occurrence.
Thankfully, you can avoid this by using the global modifier on your regular expression. Let’s add that to ours:
const regex = /freecodecamp/g;
const str = "freecodecamp is the best we love freecodecamp";
const matched = str.match(regex);
const replaced = str.replace(regex, "freeCodeCamp");
console.log(matched);
console.log(replaced);
And confirm the result:
// [ 'freecodecamp', 'freecodecamp' ]
// freeCodeCamp is the best we love freeCodeCamp
That worked! Our replace call replaced all of the lowercase freecodecamp
strings, and our match()
method matched both of them.
What’s interesting here is that when you use the global modifier with match()
, you lose the extra information about capture groups and string indices that would come in the match array.
Thankfully, 2019’s ECMAScript update brought us two new methods: matchAll()
and replaceAll()
. Like their singular counterparts, these methods accept a string or regular expression, and replaceAll()
also accepts a second argument as the string to replace with.
But unlike the previous methods, replaceAll()
and matchAll()
will throw an error if you give them a regular expression without the global modifier. Let’s update our code to use these new methods:
const pattern = "freecodecamp";
const str = "freecodecamp is the best we love freecodecamp";
const matched = str.matchAll(pattern);
const replaced = str.replaceAll(pattern, "freeCodeCamp");
console.log(matched);
console.log(replaced);
And our result:
// {}
// freeCodeCamp is the best we love freeCodeCamp
Good news! Our replaceAll()
worked exactly as we wanted – it replaced all occurrences of the lowercased freecodecamp
with the properly camelCased version.
But what is that empty object? Well, matchAll()
returns a special type of object called an Iterator
, which the freeCodeCamp console isn’t prepared to handle.
If we peek in our browser console, the Iterator
has a next()
method, which we can call to get the next value:
// RegExpStringIterator { }
// <prototype>: RegExp String Iterator {
// next: ƒ next(),
// Symbol(Symbol.toStringTag): "RegExp String Iterator"
// <prototype>: Object { ... }
// }
Let’s go ahead and call matched.next()
, and log the result:
// {
// "done": false,
// "value": [
// 0: "freecodecamp"
// groups: undefined
// index: 0
// input: "freecodecamp is the best we love freecodecamp"
// ]
// }
There’s our match array! next()
gives us an object with two values: done
, which is false
when there are more elements available in the iterator, and value
which is the value we just iterated over. So, if we call it one more time:
const regex = /freecodecamp/g;
const str = "freecodecamp is the best we love freecodecamp";
const matched = str.matchAll(regex);
const replaced = str.replaceAll(regex, "freeCodeCamp");
console.log(matched);
console.log(replaced);
console.log(matched.next());
console.log(matched.next());
// {
// "done": false,
// "value": Array [ "freecodecamp"]
// }
//
// {
// "done": false,
// "value": Array [ "freecodecamp"]
// }
Wait, why does it say done
is still false
? There should only be two matches in the array, right? Let’s call it a third time and see what we get:
// {
// "done": false,
// "value": Array [ "freecodecamp"]
// }
//
// {
// "done": false,
// "value": Array [ "freecodecamp"]
// }
//
// {
// "done": true,
// "value": undefined
// }
done
is finally true
, but why is that value undefined
? Well, as it turns out, the matchAll()
iterator is lazy. It doesn’t find all of your matches at once. It only finds a match when you tell it to by calling next()
.
As long as it finds a match, it isn’t done
. Once it fails to find a match and brings back undefined
, it is done
. This may seem inconvenient, but it can be quite helpful when your regular expression is computationally expensive.
If your example is less so, like ours, you can skip that feature and extract all of the matches at once by converting it to an array. This is achieved by calling Array.from()
and passing your iterator as the argument.
Let’s update our code to use that – we’ll go ahead and clean up our replaceAll
calls since we know that works:
const regex = /freecodecamp/g;
const str = "freecodecamp is the best we love freecodecamp";
const matched = str.matchAll(regex);
console.log(Array.from(matched));
And we finally get our array of matches:
// [
// 'freecodecamp',
// index: 0,
// input: 'freecodecamp is the best we love freecodecamp',
// groups: undefined
// ]
//
// [
// 'freecodecamp',
// index: 33,
// input: 'freecodecamp is the best we love freecodecamp',
// groups: undefined
// ]
These powerful methods can help you manipulate and extract data from strings without having to sacrifice performance or readability.