用户在搜索框里敲一个点,你的正则就匹配错了——ES2025 用 RegExp.escape() 把这件事从根上原生化了
用户在搜索框里敲一个 .,你的搜索就悄悄匹配错了——这不是你的正则写错了,是你把用户输入直接塞进了 new RegExp(),而语法字符没被当成字面量。
一个真实到不能再真实的 bug:用户搜 a.b,你写了 new RegExp(userInput, 'g'),结果 a.b、axb、aXb 全被高亮。用户搜 $9.99,正则里的 $ 被当成行尾锚点,直接匹配出一堆诡异结果。更糟的是用户搜 ((((,你的 new RegExp() 当场抛 SyntaxError,整个页面白屏。
过去十年,这个坑全靠自己手写一个转义函数兜着:
// 网上抄来抄去的那一行
function escapeRegExp(str) {
return str.replace(/[.*+?^${}()|[]\]/g, '\$&');
}
这行代码看着人畜无害,但它漏了字符。它没转义 -,也没转义 /。当你把转义结果塞进字符类 [...] 里时,- 会变成区间运算符,a-z 就不是「a、横杠、z」而是「a 到 z」;当你把结果放进正则字面量(比如要动态拼一个 new RegExp 再 eval)时,/ 会提前把字面量截断。这就是 MDN 那句反复强调的话:别自己用 replaceAll 在语法字符前面加 重新实现一遍。
官方的答案:RegExp.escape()
RegExp.escape() 是 ES2025 正式进标准库的静态方法,Baseline 2025——Chrome 136、Edge 136、Firefox 134、Safari 18.2,2025 年 5 月起所有现代浏览器都可用了。到 2026 年的今天,你完全可以不写任何 polyfill 直接用它。
const safe = RegExp.escape(userInput);
const re = new RegExp(safe, 'gi'); // 用户输入被完整当成字面量
它做的事,比手写函数多得多:
- 正则语法字符(
^ $ . * + ? ( ) [ ] { } |以及/):前面插一个。 - 其他标点(
, - = , # & ! % : ; @ ~ '“):用x十六进制转义,因为像–这种字符直接用–在/u` 模式下是语法错误。 - 字符串首字符:如果它是数字或字母,会先用
x转义。这一步专门防「拼进更大模式后被误读」——比如你的转义结果紧跟在1、x0、u000后面时,首字符不至于被吃进别人的转义序列。 - 空白与换行:空格转成
x20,u2028/u2029这类非 ASCII 行分隔符转成uXXXX,孤立代理项(lone surrogate)也一并处理。
几个官方示例的输出,能直观看到它有多「洁癖」:
RegExp.escape("foo.bar") // "\x66oo\.bar"
RegExp.escape("foo-bar") // "\x66oo\x2dbar"
RegExp.escape("foouD800bar") // "\x66oo\ud800bar"
注意 foo 的首字母 f 都被转成了 x66——这不是画蛇添足,是它要保证生成结果在任何嵌入上下文里都安全,而不只是「单独丢进 new RegExp 能用」。
它解决的不是「能不能匹配」,而是「安不安全」
把用户输入塞进 new RegExp() 不只是正确性问题,还是一个 ReDoS 面。用户构造的恶意正则(比如 (a+)+$)撞上灾难性回溯,能让你的单线程主线程卡死好几秒。RegExp.escape() 不能替代对正则本身的安全审查——按 OWASP 的建议,不可信的正则模式仍然需要超时或 RE2 这类线性引擎。但它至少把「用户数据=模式语法」这条最容易被忽略的越权路径堵死了:用户输入永远只是字面量,不可能变成元字符。
一个正确的搜索高亮实现长这样:
function highlight(text, term) {
if (!term) return text;
const pattern = new RegExp(RegExp.escape(term), 'gi');
return text.replace(pattern, (m) => `<mark>${m}</mark>`);
}
highlight('Price: $9.99 (was $12.00)', '$9.99');
// "Price: <mark>$9.99</mark> (was $12.00)"
$9.99 里的 $ 和 . 都只按字面量匹配,不会再误伤旁边的文本。
落地:今天就做这三步
- 全局搜
new RegExp(——凡参数里带变量、且这个变量可能来自用户输入的,全部包一层RegExp.escape()。 - 删掉你自己那行
escapeRegExphelper。手写版本会随着新语法(比如 ES2024 的/v模式)悄悄过时,引擎维护的RegExp.escape()永远和当前语法对齐。 - 给不带转义的正则加上约束。如果某处确实需要「用户输入当模式用」(高级搜索语法),那就要单独走沙箱/超时,别和普通搜索混在一起。
一句话结论
RegExp.escape() 是那种「看起来没必要,其实救过很多人」的标准库补充。它 2025 年就 Baseline 了,你的替代品(手写的那一行)从设计上就覆盖不全边界情况。下次再写「用用户输入拼正则」,先把 RegExp.escape() 加上——这可能是你今天改动最小、收益最大的一个提交。
评论区
登录后可评论。