首页 / 资讯中心 / 文章详情

Modern JavaScript Tutorial:正则表达式中的贪婪与懒惰量词(Greedy and Lazy Quantifiers)完全指南

Modern JavaScript Tutorial:正则表达式中的贪婪与懒惰量词(Greedy and Lazy Quantifiers)完全指南 ★ FEATURED ARTICLE
文档/教程前端【免费下载链接】en.javascript.infoModern JavaScript Tutorial项目地址https://gitcode.com/gh_mirrors/en/en.javascript.info点击查看免费下载导读在 JavaScript 正则表达式中量词quantifier默认以贪婪模式工作它会尽可能多地匹配字符直到无法继续再通过回溯backtracking逐字缩短匹配以迁就模式剩余部分。这种先吃满、再吐回的策略常常导致意外结果——例如/ ./g会把两个被引号包裹的单词连同中间文本匹配成一个整体。本篇文章基于 Modern JavaScript Tutorial 仓库中 9-regular-expressions/10-regexp-greedy-and-lazy/article.md 的完整讲解逐步剖析贪婪搜索的匹配算法、回溯原理以及如何通过在量词后追加?开启懒惰模式lazy mode并给出排除字符类这一替代方案与真实 HTML 场景的实战演练。读完本文你将能准确预测任意含量词正则的匹配结果并针对文本提取任务选出正确的量词策略。从一个小任务开始为什么/ ./是错的设想这样一个任务把文本中所有形如...的引号替换成排版上更优的角引号«...»。例如Hello, world→«Hello, world»其他语言还有各自的引号约定例如波兰语的„Witaj, świecie!”、中文的「你好世界」但本例我们统一使用«...»。第一步自然是定位被引号包裹的字符串。直觉上正则/ ./g一个引号然后任意字符再一个引号看起来非常合适但它并不正确let regexp /./g; let str a witch and her broom is one; alert( str.match(regexp) ); // witch and her broom结果令人意外本应得到两个匹配witch和broom实际却只得到一个——witch and her broom中间的文本被一并吞了进去。问题根源正是贪婪greediness。正如原文档所调侃的greediness is the cause of all evil贪婪是万恶之源。贪婪搜索引擎到底在做什么要理解量词为什么失控需要掌握正则引擎的匹配算法。整体流程如下从字符串的每个位置开始尝试匹配在该位置尝试匹配模式若失败移动到下一个位置继续尝试。这串平实的描述掩盖了真正的玄机下面以模式.为例逐字追踪它在字符串a witch and her broom is one上的搜索过程详见 article.md 的逐步图示。寻找模式的第一个字符引擎从字符串第 0 个位置开始那里是字符a匹配失败依次后移最终在第 3 个位置找到了引号见 witch_greedy1.svg。匹配点号.接下来模式要求匹配任意字符除换行外。当前位置的下一个字符w满足要求匹配成功。量词贪婪地重复点号引擎一个字符接一个字符地吞入……直到哪里为止由于点号几乎匹配所有字符它一直吃到字符串的末尾见 witch_greedy3.svg。模式剩余部分无法匹配结束后模式的下一个字符是但字符串已经结束没有更多字符了。引擎意识到自己吃太多开始回溯backtracking——把量词的匹配缩短一个字符再从那个位置尝试匹配模式剩余部分。逐字缩短、反复试探最后一个字符是e不是引号再缩短一个字符末尾是n仍不是引号……引擎持续回溯直到模式剩余部分本例中的恰好匹配为止见 witch_greedy6.svg。匹配完成第一个匹配结果是witch and her broom。由于正则带有g标志搜索从该匹配的结束位置继续剩余字符串is one中再无引号因此没有任何更多结果。核心结论在默认的贪婪模式下带量词的字符会被尽可能多地重复。引擎先尽可能吞入字符若模式剩余部分匹配不上再逐步吐回。用一张图直观感受贪婪模式下量词的范围差异——贪婪点号会一路覆盖到字符串末尾才被迫回溯懒惰模式量词后加一个?懒惰模式是贪婪模式的对立面含义是重复最少次数。开启方式非常简单在量词后面追加一个问号?使其变为*?、?甚至对?自身也适用??。需要澄清一个容易混淆的点问号?本身是一个量词表示零次或一次但当它跟在另一个量词甚至它自己之后时含义完全不同——它把匹配模式从贪婪切换为懒惰。懒惰版正则会得到预期结果let regexp /.?/g; let str a witch and her broom is one; alert( str.match(regexp) ); // witch, broom懒惰搜索的逐步追踪第一步与贪婪版完全相同在第 3 个位置找到起始引号。点号同样匹配了一个字符w。从这里开始分道扬镳因为?处于懒惰模式引擎不再尝试让点号多匹配一个字符而是立刻尝试匹配模式剩余部分见 witch_lazy3.svg。当前位置是i不匹配失败。失败后引擎才增加一次点号的重复再次尝试匹配见 witch_lazy4.svg。如此反复多匹配一个字符 试探剩余模式直到找到为止见 witch_lazy5.svg。下一次搜索从当前匹配的结束位置开始继续得到第二个结果见 witch_lazy6.svg。*?与??的工作方式完全一致只有当模式剩余部分在当前步无法匹配时引擎才增加重复次数。关键误区懒惰只对加了?的量词生效懒惰并非全局开关——它只对紧跟?的那个量词生效其余量词继续保持贪婪。看这个例子alert( 123 456.match(/\d \d?/) ); // 123 4逐步拆解模式\d处于贪婪模式尽可能多匹配数字得到123遇到空格后停止。模式中的空格与字符串中的空格匹配。轮到\d?它是懒惰的只匹配一个数字4然后立刻检查模式剩余部分——但\d?之后没有任何内容了。懒惰模式不会无谓地多重复模式到此结束得到匹配123 4。附带练习/\d? \d?/g匹配什么仓库配套练习 1-lazy-greedy/task.md 正是这一机制的测验alert( 123 456.match(/\d? \d?/g) ); // ?答案仍是123 4见 solution.md第一个\d?虽然是懒惰的但为了到达模式中的空格它必须一路匹配到123第二个\d?只需一个数字4就已足够。懒惰不等于尽可能少而是恰好满足模式需要的最少次数。优化提示现代正则引擎内部会对算法进行优化以提升速度实际执行可能与上述描述略有出入。但理解正则的工作原理、编写可靠的正则表达式并不需要了解这些内部优化——它们只服务于性能。复杂的正则表达式很难被优化因此其搜索过程往往与上文描述完全一致。替代方案排除字符类[^]处理这类问题往往不止一条路。在找引号对的场景中不用懒惰模式也能实现[^]。let regexp /[^]/g; let str a witch and her broom is one; alert( str.match(regexp) ); // witch, broom它的原理是先匹配一个引号随后匹配一个或多个非引号字符[^]最后匹配闭合引号。当引擎在[^]中遇到闭合引号时自动停止重复任务完成。请务必注意这个方案并不取代懒惰量词它们是不同的手段各有适用场景。下面是懒惰量词失效、而该方案依然正确的经典例子。实战案例解析 HTML 链接目标是找出形如a href... classdoc的链接href属性值任意。方案一贪婪.*彻底失败let str ...a hreflink1 classdoc... a hreflink2 classdoc...; let regexp /a href.* classdoc/g; // Whoops! Two links in one match! alert( str.match(regexp) ); // a hreflink1 classdoc... a hreflink2 classdoc与女巫例子如出一辙.*贪婪地吃掉了太多字符把两个链接连同中间文本匹配成了一个a href..................................... classdoc a hreflink1 classdoc... a hreflink2 classdoc方案二懒惰.*?单链接文本有效……let str ...a hreflink1 classdoc... a hreflink2 classdoc...; let regexp /a href.*? classdoc/g; // Works! alert( str.match(regexp) ); // a hreflink1 classdoc, a hreflink2 classdoc看起来已经正确得到两个匹配a href..... classdoc a href..... classdoc a hreflink1 classdoc... a hreflink2 classdoc方案二变体换一个输入懒惰也失效let str ...a hreflink1 classwrong... p style classdoc...; let regexp /a href.*? classdoc/g; // Wrong match! alert( str.match(regexp) ); // a hreflink1 classwrong... p style classdoc为什么失败追踪过程先找到链接起始a href。.*?懒惰地每次只吞入一个字符并检查后面是否匹配 classdoc。一路试探下去直到在另一个标签p内部的 classdoc处才凑齐剩余模式。也就是说匹配越过了链接a...的边界跑到了后面的p标签里——这不是我们想要的a href................................... classdoc a hreflink1 classwrong... p style classdoc结论无论贪婪还是懒惰/a href.* classdoc/式的模式都有问题。正确做法是限定href属性内部只能是非引号字符href[^]*。它会从最近的引号处截断恰好取完属性值。let str1 ...a hreflink1 classwrong... p style classdoc...; let str2 ...a hreflink1 classdoc... a hreflink2 classdoc...; let regexp /a href[^]* classdoc/g; // Works! alert( str1.match(regexp) ); // null, no matches, thats correct alert( str2.match(regexp) ); // a hreflink1 classdoc, a hreflink2 classdoc注意str1的结果为null无匹配这正是期望行为——link1的class是wrong而非doc不该被匹配。配套练习在实战中巩固两种模式仓库在该章节下提供了三个配套任务可用于检验对贪婪/懒惰量词的理解路径见 10-regexp-greedy-and-lazy。练习一贪婪 vs 懒惰的直观对照任务 1-lazy-greedy/task.md 要求预测123 456.match(/\d? \d?/g)的结果答案123 4已在前面解析solution.md。它最能说明懒惰 满足模式需求的最少次数这一本质。练习二找出 HTML 注释任务 3-find-html-comments/task.md 要求找出文本中所有 HTML 注释let regexp /your regexp/g; let str ... !-- My -- comment test -- .. !---- .. ; alert( str.match(regexp) ); // !-- My -- comment \n test --, !----答案solution.md是!--.*?--加标志slet regexp /!--.*?--/gs; let str ... !-- My -- comment test -- .. !---- .. ; alert( str.match(regexp) ); // !-- My -- comment \n test --, !----两个要点懒惰量词*?使点号恰好在--前停下标志s让点号能匹配换行符否则跨越多行的注释无法被找到。这也呼应了前文点号默认不匹配换行的约束。练习三匹配所有 HTML 标签任务 4-find-html-tags-greedy-lazy/task.md 要求找出所有开/闭标签及其属性并假设属性值内不含和简化处理let regexp /your regexp/g; let str a href/ input typeradio checked b; alert( str.match(regexp) ); // a href/, input typeradio checked, b答案是排除字符类方案solution.mdlet regexp /[^]/g; let str a href/ input typeradio checked b; alert( str.match(regexp) ); // a href/, input typeradio checked, b[^]保证标签内容中不出现尖括号同时这种空标签因要求至少一个字符而被自然排除——这再次印证精确排除边界字符常常优于贪婪 回溯的组合。总结量词有两种工作模式选择哪一种取决于你希望它吃多少模式行为开启方式适用场景贪婪Greedy尽可能多地重复量词必要时回溯缩短默认需要取到边界内最远内容的场景需配合精确边界懒惰Lazy每次重复前先尝试匹配模式剩余部分失败才增加一次量词后加?如*?、?、??希望匹配尽快闭合的场景如.*?贪婪模式下引擎先让带量词的字符尽可能多地重复无法继续匹配模式剩余部分时再逐步减少重复次数回溯重新尝试。例如\d会吞掉所有连续数字。懒惰模式由量词后的问号?开启引擎在每次重复带量词字符之前都会先尝试匹配模式剩余部分。懒惰并非万能药正如本文所示它可能越过真实边界HTML 链接案例此时精调过的贪婪搜索 排除字符类如[^]、[^]往往是更可靠的方案。理解贪婪、懒惰与回溯三者的关系是编写健壮文本提取正则的基石。建议直接在本仓库的 article.md 中配合 SVG 追踪图逐帧阅读并在浏览器控制台亲手运行上述代码示例观察匹配结果与预期之间的差异。赞分享文档/教程前端【免费下载链接】en.javascript.infoModern JavaScript Tutorial项目地址https://gitcode.com/gh_mirrors/en/en.javascript.info点击查看免费下载相关推荐JavaScript正则表达式中的贪婪与懒惰模式详解JavaScript正则表达式中的贪婪与懒惰模式详解 正则表达式是JavaScript中强大的文本处理工具而量词 quantifiers 的使用则是正则表达式文档/教程前端JavaScript 正则表达式中的贪婪模式与懒惰模式详解JavaScript 正则表达式中的贪婪模式与懒惰模式详解 正则表达式是 JavaScript 中强大的文本处理工具而理解量词quantifiers的贪婪JavaScript 正则表达式终极指南贪婪与懒惰模式全解析JavaScript 正则表达式终极指南贪婪与懒惰模式全解析 开篇痛点直击 你是否曾因正则表达式匹配结果与预期大相径庭而抓狂当面对嵌套引号、HTML标签或复创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
阅读完成 · 觉得有帮助?
咨询建站