Note: Whether something is a "regex" can be duck-typed. It doesn't have to be a RegExp
!
Some built-in methods would treat regexes specially. They decide whether x
is a regex through multiple steps:
-
x
must be an object (not a primitive). - If
x[Symbol.match]
is not undefined
, check if it's truthy. - Otherwise, if
x[Symbol.match]
is undefined
, check if x
had been created with the RegExp
constructor. (This step should rarely happen, since if x
is a RegExp
object that have not been tampered with, it should have a Symbol.match
property.)
Note that in most cases, it would go through the Symbol.match
check, which means:
- An actual
RegExp
object whose Symbol.match
property's value is falsy but not undefined
(even with everything else intact, like exec
and @@replace
) can be used as if it's not a regex. - A non-
RegExp
object with a Symbol.match
property will be treated as if it's a regex.
This choice was made because @@match
is the most indicative property that something is intended to be used for matching. (exec
could also be used, but because it's not a symbol property, there would be too many false positives.) The places that treat regexes specially include:
For example, String.prototype.endsWith()
would coerce all inputs to strings, but it would throw if the argument is a regex, because it's only designed to match strings, and using a regex is likely a developer mistake.
"foobar".endsWith({ toString: () => "bar" });
"foobar".endsWith(/bar/);
You can get around the check by setting @@match
to a falsy value that's not undefined
. This would mean that the regex cannot be used for String.prototype.match()
(since without @@match
, match()
would construct a new RegExp
object with the two enclosing slashes added by re.toString()
), but it can be used for virtually everything else.
const re = /bar/g;
re[Symbol.match] = false;
"/bar/g".endsWith(re);
re.exec("bar");
"bar & bar".replace(re, "foo");