Champion: Mathias Bynens (Google, @mathiasbynens).
This proposal is at stage 1 of the TC39 process.
Currently there is no way to replace all instances of a substring in a string without use of a global regexp.
String.prototype.replace
only affects the first occurrence when used with a string argument. There is a lot of evidence that developers are trying to do this in JS — see the StackOverflow question with thousands of votes.
Currently the most common way of achieving this is to use a global regexp.
const queryString = 'q=query+string+parameters';
const withSpaces = queryString.replace(/\+/g, ' ');
This approach has the downside of requiring special RegExp characters to be escaped — note the escaped '+'
.
An alternate solution is to combine String#split
with Array#join
:
const queryString = 'q=query+string+parameters';
const withSpaces = queryString.split('+').join(' ');
This approach avoids any escaping but comes with the overhead of splitting the string into an array of parts only to glue it back together.
We propose the addition of a new method to the String prototype - replaceAll
. This would give developers a straight-forward way to accomplish this common, basic operation.
const queryString = 'q=query+string+parameters';
const withSpaces = queryString.replaceAll('+', ' ');
It also removes the need to escape special regexp characters (note the unescaped '+'
).
The proposed signature is the same as the existing String.prototype.replace
method:
String.prototype.replaceAll(searchValue, replaceValue)
searchValue
throws if it is a RegExp (there's no reason to usereplaceAll
with a RegExpsearchValue
). Otherwise, the remaining algorithm usesToString(searchValue)
. This option can be implemented very efficiently.
Alternative 1.1: Unconditionally use ToString(searchValue)
, even if searchValue
is a RegExp. Doesn't seem like a good option since this will break RegExp args in unexpected ways (e.e. /./.toString() // --> "/[.]/"
).
Alternative 1.2: If searchValue
is a RegExp, create a clone including the 'g'
flag and dispatch to RegExp.prototype[@@replace]
. Otherwise, use ToString(searchValue)
. There's precedent for this in RegExp.prototype[@@split]
. This option seems consistent with user expectations; but we lose efficiency & simplicity on the implementation side, and we create an unexpected performance trap since cloning the regexp instance is slow.
- The algorithm uses
ToString(replaceValue)
and neither implementsGetSubstitution
semantics nor allows callablereplaceValue
. Thereplace
function's interface is (perhaps unnecessarily) complex. We can take this opportunity to simplify, resulting in less cognitive load for users & simpler, more efficient implementations for VMs.
Alternative 2.1: As above, but implement GetSubstitution
for more consistency with replace
.
Alternative 2.2: As 2.1, but additionally allow callable replaceValue
for more consistency with replace
. Both 2.1 and 2.2 add complexity and overhead to the implementation.
Note that if alternative 1.2 is chosen above, then we need to support GetSubstitution
semantics for replaceValue
for consistency.
- There will be no new
RegExp.prototype[@@replaceAll]
function. Depending on the chosen solution forsearchValue
, RegExp arguments either throw or are forwarded to@@replace
.
- Java has
replaceAll
, accepting a regexp (the actual param is a string, so simply putting your substring as the first param does the same thing we are proposing). - Python
replace
replaces all occurrences, but accepts an optional param to limit the number of replacements. - PHP has
str_replace
which has an optional limit parameter like python. - Ruby has
gsub
, accepting a regexp or string, but also accepts a callback block and a hash of match -> replacement pairs.
Q: What are the main benefits?
A: A simplified API for this common use-case that does not require RegExp knowledge. A way to global-replace strings without having to escape RegExp syntax characters. Possibly improved optimization potential on the VM side.
Q: What about adding a limit
parameter to replace
instead?
A: This is an awkward interface — because the default limit is 1, the user would have to know how many occurrences already exist, or use something like Infinity.
- none yet