从《JavaScript语言精粹》到《你不知道的JS》——深入理解正则表达式

1 阅读10分钟

从《JavaScript语言精粹》到《你不知道的JS》——深入理解正则表达式

前端工程师也许最头疼、也最离不开的工具就是正则表达式了。它是魔鬼还是天使?我想答案应该介于两者之间。

写在前面

翻开《JavaScript语言精粹》,第七章赫然写着“正则表达式”。这本书是前端圈公认的“圣经”之一,薄薄一百多页,全是干到发硬的技术精华。而正则表达式这一章也不例外,它用一个长达数行的复杂例子,直接向读者展示了这门“语言中的语言”的强大与晦涩。

与此同时,另一本备受推崇的《你不知道的JavaScript》中卷,则专门用一节篇幅讲述了ES6中为正则表达式带来的一系列革新。

那么,当这两部经典的观点与内容交织在一起时,能碰撞出怎样的火花?我今天就带大家通过它们,重新走进正则表达式的世界。

正则表达式:诞生自数学的字符串的专属语言

在深入之前,我们先来看看它的“血统”。正如《JavaScript语言精粹》第七章开头所提:“JavaScript 的许多特性都借鉴自其他语言。语法借鉴自Java,函数借鉴自Scheme,原型继承借鉴自Self。而 JavaScript 的正则表达式特性则借鉴自 Perl”。

Perl 在文本处理领域的地位举足轻重。JavaScript 直接从这位“巨人”的肩膀上拿来了这套语法,这意味着如果你平时用正则用得少,初次接触可能会觉得它和普通的 if/else 编程逻辑不太一样。它是一套专门的匹配模式(Pattern) ,更像是定义了一套“字符串检索规则”。

而且你可能没有意识到,正则表达式在 JavaScript 中也是一种对象,它有着自己专属的属性和方法。我们可以通过字面量 /pattern/ 或者构造函数 new RegExp('pattern') 来创建它。

这两本书都强调了一个核心观点:正则虽然难,但对于处理像数据验证、复杂的字符串查找和替换这类工作,它能用比普通字符串函数高得多的性能解决常规方法绕很多弯路才能处理的问题。

但是,老道的开发者都知道:“正则表达式,不只是用来匹配的,更是用来避免过度匹配的”。

古老语法下的新活力:ES6 的革新

如果你读过《你不知道的JavaScript》,你会发现这本书一直引导读者从更深层次去理解语言机制,正则也不例外。实际上,JavaScript 的正则很长一段时间内缺乏大的变化,直到 ES6 的推出才带来了一些值得关注的新特性。

“ES6 为正则表达式增添了一些新的修饰符和属性,使得这门古老的字符串处理语言在今天依然充满活力。”

1. 粘性修饰符 y —— 让你的匹配更精准

《你不知道的JavaScript》详细解释了 y 修饰符,也被称为“粘性”匹配。它的行为和 ^ 在全局匹配中有本质不同。y 修饰符要求匹配必须从 lastIndex 指定的位置开始,并且匹配的结果必须紧跟着这个位置。

场景化理解:

let str = "Hello, Hello, World!";
let regex = /Hello/y;
regex.lastIndex = 7; // 指定从索引 7 处开始匹配(即第二个 H 的位置)
console.log(regex.exec(str)); // 返回匹配结果 "Hello"
regex.lastIndex = 1; // 如果从空格或非 H 位置开始
console.log(regex.exec(str)); // 返回 null

在很多需要分词扫描(Tokenization)的场景中,比如实现一个微型编译器或者日志解析器,y 修饰符能确保你的匹配逻辑不会因为字符串内部的噪声而产生误匹配。

2. Unicode 修饰符 u —— 拥抱国际化

《你不知道的JavaScript》特别指出:过去,JavaScript 正则只能识别基本多文种平面(BMP)中的字符,那些需要用 4 个字节表示的特殊字符(比如某个罕见的中文字符或表情符号),默认会被当作两个独立的字符去匹配。

对于需要在 JavaScript 中处理国际化或一些特殊字符的前端来说,u 修饰符是刚需。它开启了完整的 Unicode 匹配模式,让正则可以正确地将那些四个字节长的字符识别为一个单元。

// 没有使用 u 修饰符,错误匹配
/^.$/.test("𠮷"); // false,因为 "𠮷" 被当作两个字符
// 使用了 u 修饰符,正确匹配
/^.$/u.test("𠮷"); // true

在全球化的今天,如果你在开发多语言项目或者表单中允许用户输入特殊表情符号,务必记住加上 u 修饰符,它会帮你避免很多看似奇怪又难以调试的 Bug。

性能的暗礁:灾难性回溯

《你不知道的JavaScript》以及现代前端的一些性能分析实践,揭示了正则表达式一个非常隐蔽的“杀手”——性能陷阱。

当我们写下一个正则表达式时,绝大部分 JavaScript 开发者都知道它的强大,但很少人会去思考它的执行机制。然而这些引擎的机制,稍有不慎就会把你拖入性能的无底洞。

JavaScript 的正则引擎通常采用的是 NFA(非确定性有限状态自动机)。为了理解它,你可以想象一个在迷宫里寻找出口的人。

  • DFA(确定性有限状态自动机) :拿着手电筒过迷宫,每走一步,他都明确知道自己下一步要去哪里,只看一次路,O(n) 时间完成。
  • NFA(非确定性有限状态自动机) :同样过迷宫,每当遇到岔路口,他就在脑海里“分叉”出另一个自己同时往下走。一旦某条路走不通,他就回溯到上一个岔路口选择另一条路。

这种机制本身没有问题,但当遇到带有大量重叠分支的正则表达式时,这个探索过程就会呈指数级膨胀,导致引擎“卡死”在这个迷宫的分叉里——这就是著名的灾难性回溯

// 一个典型的危险正则
const regex = /(a+)+b/;
// 让它去匹配 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaac'
// 注意最后是 c 不是 b

当正则引擎在处理上述模式时,a+ 匹配了一大堆 a,后面的 (a+)+ 又可以拆成无数种组合去吃掉这些 a,最后发现不是 b,只能吐出一个 a 换另一种分法……这个过程会指数级增长,CPU 瞬间飙升,Node.js 的事件循环会被完全阻塞,造成拒绝服务攻击。

这种漏洞并非纸上谈兵。2026 年 4 月,Signal K Server 的 WebSocket 订阅处理逻辑就被曝出存在正则可被注入元字符的严重漏洞,攻击者构造恶意字符串即可触发灾难性回溯,导致服务器 CPU 飙升至 100%,API 和 WebSocket 完全无响应。这种漏洞的 CVSS 评分高达 7.5,属于高危级别。

当《语言精粹》遇上性能陷阱

有意思的是,《JavaScript语言精粹》第七章里正好展示了一个复杂的正则表达式,用于解析 URL,里面大量用到了量词如 ?*+ 以及括号分组。虽然案例本身运行良好,但从现代安全视角来看,它展示了正则引擎究竟能有多复杂。编写正则时,那些看似精简的嵌套量词,可能就是明天被恶意用户利用从而击垮服务器的定时炸弹。

如何安全、优雅地使用正则

结合这两本书的内容和现代前端安全意识,为了让写出高效、稳健、可读且安全的正则,我们有几个建议可以参考:

1. 优先使用非捕获组来提升匹配效率

《JavaScript语言精粹》把捕获分组比作“记忆”,它会消耗额外的内存去记住匹配到的内容,当你只是想单纯地判断格式或提取部分字段,可以直接用 (?:...) 非捕获分组。

// ❌ 如果我们不关心数字分组,这里浪费了内存
const badRegex = /(\d{4})-(\d{2})-(\d{2})/;
// ✅ 非捕获分组,仅用于匹配
const goodRegex = /(?:\d{4})-(?:\d{2})-(?:\d{2})/;

2. 尽量使用前瞻/后瞻断言

《你不知道的JavaScript》并未详细展开,但在日常编码中,零宽断言(Lookahead/Lookbehind)能帮我们写出更优雅的代码,而不用写成复杂的正则劫持逻辑,它不消耗字符。

// 数字后跟着 '%' 或 'px',但又不希望捕获百分号或单位
const regex = /\d+(?=%)|(?<=px)\d+/;

3. 正则治理 —— 安全检查与复杂度限流

最重要的一点是永远不要直接用用户输入构造动态的正则表达式。如果迫不得已需要在服务端处理动态规则,务必对输入做清洗和过滤。

另一方面,在处理大规模文本时,可以对超长文本进行分片或者长度限制,防止正则引擎因为匹配超大字符串而发生资源消耗。

4. 使用现代工具辅助分析

可以多利用类似 regex101.com 这类在线网站进行正则调试,这类网站会以图示方式展示你的表达式每一步的执行逻辑。如果看到工具中的回溯步骤过多,或者出现红色的警告,说明你的正则很可能存在性能隐患。

5. 善用现代 JS 引擎的性能优化

V8 引擎一直在优化正则表达式的执行效率。从 V8 7.8 版本开始,正则表达式先用字节码解释执行以节省内存,当同一模式被频繁使用时,会被重新编译为原生代码来加速执行。这意味着无需过度担心微优化——先把代码写正确,引擎会在背后帮你处理大部分性能问题。

结语

从《JavaScript语言精粹》中的精悍案例,到《你不知道的JavaScript》里引擎底层的科普以及 ES6 的新特性,我们可以很清晰地看到,正则表达式的知识体系是非常完整的: 它既需要开发者理解复杂的语法规则,又要求开发者对背后的匹配引擎和性能隐患有深刻的理解

所以下次面临像密码强度校验、URL 截取或者 Markdown 解析这类需求时,别忘了正则表达式的力量。不过在敲下诸如 (a+)+b 这种模式之前,不妨多思考一下:它是一个可靠的匹配机器,还是一个随时可能阻塞事件循环的定时炸弹

毕竟,用得好,它是替代几十行遍历代码的简洁利器;用不好,它可能是藏着灾难性回溯、让用户体验与安全性直接葬送在连环岔路口的“性能杀手”。希望每一位开发者都能安全、优雅地驾驭这门“字符串的专属语言”。


你可能还喜欢:

(本文内容结合《JavaScript语言精粹》第7章、《你不知道的JavaScript(下卷)》第2.10节内容撰写)