雜記 › 技術筆記 › 正則測試頁永遠帶 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