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");