杂记 › 技术笔记 › 正则测试页永远带 g:填 gg 直接报错、填大写静默失效,21 处带组匹配只列 20 行

正则测试页永远带 g:填 gg 直接报错、填大写静默失效,21 处带组匹配只列 20 行

flags 框先被 6 个字母的白名单洗一遍,再被无条件补上 g:填 GIM 静默失效、填 gg 当场报错。状态行、高亮 mark、分组面板各记各的账,21 处带组匹配只列 20 行。

那格 flags 写着 GIM,页面却当它什么都没填

起因是我在 flags 框里敲了大写的 GIM,模式填 A,测试文本填三个 a,状态行回「无匹配」。按框里那三个字母的读法,忽略大小写早该开着。把模板(6118 字节 / 133 行)的 script 段整段摘出来、在纯 Node 里跑一圈才看清顺序:L60 先洗一遍输入,L69 再无条件补一个 g。

洗的依据是一份只有 6 个字母的白名单——g i m s u y;大写、空格、符号、汉字在构造之前就删干净。于是填 x 落到 g,填 GIM 也落到 g,填 giz 只剩 gi,填 vi 的 v 被删后落到 ig,gimsuy 是唯一一次原样通过。补的那一步更硬:空 → 实参 g;i → 实参 ig;imsuy → 实参 imsuyg,re.flags 由引擎归一成 gimsuy。这套先洗后补的写法在正则表达式测试那页谁都能亲手复现一遍。

gg 是另一类下场:不去重、不忽略,构造当场抛 Invalid flags supplied to RegExp constructor,整页转进红色叉号(U+274C)分支,结果面板 display:none。中间夹一个空格的写法会被先洗成 gg,报错是洗字符那一步自己造出来的重复。

flags 框 → 白名单洗一遍 → 补 g 后的实参 你填 洗后 送进 RegExp GIM 空 g giz gi gi vi i ig imsuy imsuy imsuyg gg gg 当场抛错

图 1:洗掉什么、留下什么、补一个字母,送进构造器的永远含 g。

补 g 不是功能开关,是 matchAll 那行在续命

动机能钉到一行代码:结果面板走 text.matchAll(re),非全局对象喂进去直接 TypeError,实测 message 原文「String.prototype.matchAll called with a non-global RegExp argument」。同一个 /a/ 对 aaa,不带 g 展开即抛,带 g 得 3 条。所以补 g 是那行代码的续命写法,不是留给用户的开关。i m s u y 确实可开可关:模式 A 对 aaa,无 i 时 0 条、有 i 时 3 条;^b 对跨行文本,无 m 时 0 条、有 m 时 1 条。唯独 g 恒真、恒不可关。

三张面板各记各的账

状态行的数字是 matchAll 展开后的数组长度,高亮的 <mark> 外壳按这个数组逐条重复,分组面板另有 20 条上限。把模式 (a) 对着连续 a 的文本往上推档位,三本账就分开了:

匹配数状态行高亮 mark 数分组面板面板列表 innerHTML
21212120 行6021 字符
25252520 行6021 字符
40404020 行6021 字符
10001000100020 行6021 字符
10000100001000020 行6021 字符

面板末行永远停在 #20 index=19,列表长度一字不多,页面上也没有任何「仅显示前若干条」的文案。状态行少报了吗?没有,它连 10000 都照写;被切掉的 9980 条只是不出现在分组那一栏。

零宽的时候,两本账才显出区别

(?:) 对 abc(3 个可见字符)状态行写「找到 4 处匹配」,同时画 4 个内容为空的 mark,高亮 HTML 长 419 字符;\b 对 a b c 报 6 处、6 个空 mark;a* 对 baab 报 4 处,其中 3 处 0 宽、只有 1 处装着 aa。数字与绿块数始终相等,但相等不等于看得见:说有 4 处,屏幕上可能一块绿都没有。

外壳也按条付钱:开标签 97 字符、闭标签 7 字符,1 个可见字符要付 105 字符,空匹配同样付 104 字符外壳换 0 个可见字符。模式 a 对 10000 个 a:状态 10000、mark 10000、高亮 HTML 1050000 字符,而原文只有 10000 字符,膨胀 105 倍,这串还是在一次 oninput 里同步拼完再整体赋给 innerHTML 的。DOM 真正绘制要多久,本批未实测——探针只跑到字符串层,没开浏览器。

同一行里三张面板:状态行、mark 内容、分组行数 模式与文本 状态行 mark 内字符 分组面板 (?:) 对 abc 4 处 0 不显示 \b 对 a b c 6 处 0 不显示 a* 对 baab 4 处 3 处空 不显示 (a) 对 21 个 a 21 处 21 段 只列 20 行 外壳:97 + 内容 + 7 1 字符付 105 10000 字符付 1050000

图 2:末行就是标题那笔账——21 处带组匹配,面板恒 20 行。

分组面板的闸不止一道

  • 显示闸:只要有任意一条匹配带捕获组,整块面板就出来。模式 a|(b) 对 ab 状态 2、面板照常显示,但第 1 行一个组单元都没有。
  • 跳号闸:未命中的组直接不渲染。(a)?(b) 对 ab b a b ab 得 4 行,第 2、4 行整行只剩「组2:」,组1 凭空消失。
  • 匿名闸:(?<y>..) 对 abcd 只显示「组1: ab」「组1: cd」,全页没有代码读那份命名组字典,名字 y 在 UI 里等于不存在。
  • 下标账:index= 后面是原文绝对下标,不每轮从 0 起。(a) 对 xa xa xa 三行是 #1 index=1、#2 index=4、#3 index=7。

藏起来的旧账,和一笔转义字符串账

模式为空时在构造之前就 return(实测构造次数 0),两个面板一起藏掉,一行提示都不留。语法错误走红叉分支:模式 ( 的错误串实测 61 字符 / 77 字节,前缀占 9 个码元(含一个全角冒号),余下全是引擎 message;同一时刻,上一轮 210 字符的高亮和「找到 2 处匹配」的状态行仍躺在隐藏 DOM 里,只是被盖住。点「清空」也不抹:flags 被复位成 g 而不是留空,315 字符的旧状态 HTML 照样留着。

转义那条只记字符串账:escHtml 只动 & < > 三个字符,双引号原样进 innerHTML;输入里本来就写的 &amp; 会被再吃一遍,1 个 & 出 1 个 &amp;,长度 1 变 5。页面本体是 4 个函数、9 个 id,无异步、无防抖;替换在这里没有入口——既没有替换框也没有替换结果节点,要在匹配之后改内容得换批量查找替换那页去做。

Q:flags 填大写为什么一点反应都没有?

白名单只收那 6 个小写字母,大写整串在构造之前就被删掉,剩下的空串又被补成 g。页面不给提示,只能从结果反推。

Q:填 gg 报「正则语法错误」,是这页坏了吗?

不是。重复字母由构造器拒收,页面把引擎 message 原样贴出来;夹空格的写法也是先被洗成 gg 才撞上报错。

Q:分组面板只有 20 行,是不是只匹配到 20 处?

不是,状态行才是全量那本账。20 是面板那一步 slice 的上限,40 条时它只列 20 行、末行恒 #20 index=19。

Q:带 g 的正则不是有状态的吗,这里怎么没体现?

那是站内另一篇实测的范围。本页每次按键都重新构造一个 RegExp(构造次数 1 次/调用),对象不跨输入留状态,两件事别混着记。

📎 相关文章

3.7 读成 3、1e3 读成 1:分数计算器这一页先在输入端截成整数,再在 14 个字符处换显示口径 2026-11-14 后缀只切最后一个点:archive.tar.gz 分走 gz,photo. 和 .gitignore 挤进同一格,表格首列还会掉一个点 2026-11-14 星号段长写死:脱敏页五套掩码固定切出 4、3、11、12 枚星号,18 位串被手机档抢走、空框回落 * 2026-11-13 锁是单向的、百分比是两把尺:pct 50 得 619×430,PNG 四档 quality 全 6971 B 2026-11-13 相对时间的「一个月」写死 30 天:时间戳页六档除数、13 位毫秒输入与 UTC+8/UTC-5 两档截断实测 2026-11-13