那格 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,報錯是洗字元那一步自己造出來的重複。
圖 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 |
|---|---|---|---|---|
| 21 | 21 | 21 | 20 行 | 6021 字符 |
| 25 | 25 | 25 | 20 行 | 6021 字符 |
| 40 | 40 | 40 | 20 行 | 6021 字符 |
| 1000 | 1000 | 1000 | 20 行 | 6021 字符 |
| 10000 | 10000 | 10000 | 20 行 | 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 真正繪製要多久,本批未實測——探針只跑到字串層,沒開瀏覽器。
圖 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;輸入裡本來就寫的 & 會被再吃一遍,1 個 & 出 1 個 &,長度 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 次/呼叫),物件不跨輸入留狀態,兩件事別混著記。