用户在搜索框里敲一个点,你的正则就匹配错了——ES2025 用 RegExp.escape() 把这件事从根上原生化了

用户在搜索框里敲一个 .,你的搜索就悄悄匹配错了——这不是你的正则写错了,是你把用户输入直接塞进了 new RegExp(),而语法字符没被当成字面量。

一个真实到不能再真实的 bug:用户搜 a.b,你写了 new RegExp(userInput, 'g'),结果 a.baxbaXb 全被高亮。用户搜 $9.99,正则里的 $ 被当成行尾锚点,直接匹配出一堆诡异结果。更糟的是用户搜 ((((,你的 new RegExp() 当场抛 SyntaxError,整个页面白屏。

过去十年,这个坑全靠自己手写一个转义函数兜着:

// 网上抄来抄去的那一行
function escapeRegExp(str) {
  return str.replace(/[.*+?^${}()|[]\]/g, '\$&');
}

这行代码看着人畜无害,但它漏了字符。它没转义 -,也没转义 /。当你把转义结果塞进字符类 [...] 里时,- 会变成区间运算符,a-z 就不是「a、横杠、z」而是「a 到 z」;当你把结果放进正则字面量(比如要动态拼一个 new RegExpeval)时,/ 会提前把字面量截断。这就是 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 转义。这一步专门防「拼进更大模式后被误读」——比如你的转义结果紧跟在 1x0u000 后面时,首字符不至于被吃进别人的转义序列。
  • 空白与换行:空格转成 x20u2028/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 里的 $. 都只按字面量匹配,不会再误伤旁边的文本。

落地:今天就做这三步

  1. 全局搜 new RegExp(——凡参数里带变量、且这个变量可能来自用户输入的,全部包一层 RegExp.escape()
  2. 删掉你自己那行 escapeRegExp helper。手写版本会随着新语法(比如 ES2024 的 /v 模式)悄悄过时,引擎维护的 RegExp.escape() 永远和当前语法对齐。
  3. 给不带转义的正则加上约束。如果某处确实需要「用户输入当模式用」(高级搜索语法),那就要单独走沙箱/超时,别和普通搜索混在一起。

一句话结论

RegExp.escape() 是那种「看起来没必要,其实救过很多人」的标准库补充。它 2025 年就 Baseline 了,你的替代品(手写的那一行)从设计上就覆盖不全边界情况。下次再写「用用户输入拼正则」,先把 RegExp.escape() 加上——这可能是你今天改动最小、收益最大的一个提交。

评论区

0 条评论

登录后可评论。

铁锈·Rust工具链 61 阅读