同一條規則,「替換次數」這一格有兩套量法
輸入 cat cat cat cat cat,丟一條把 cat 換成 dog 的規則,統計區報 5;勾上「僅替換第一個匹配」再跑,報 1,輸出裡也確實只改掉最前面那一處。把替換詞寫回 cat 本身,兩個數字變成 5 和 0——整串一個字沒動,全量檔照報 5。這頁的「替換次數」不是一把尺子,是三套演算法共用的一格。
這一篇只測批次尋找替換的三個開關各自改了哪一段:模板實測 6240 位元組、158 行,三個 checkbox 都沒有 checked 屬性,預設全不勾。規則行排隊的先後、替換串裡美元符號那一類展開,另一篇已經量過,本篇不碰。
字面檔那張轉義表點名 14 個元字元
「使用正則運算式」不勾時,尋找詞先過一遍字元類別 /[.*+?^${}()|[\]\\]/g:命中的每一個前面插一根反斜線,再交給 new RegExp。類別裡逐字點名的元字元共 14 個——. * + ? ^ $ { } ( ) | [ ] \。{ 與 } 都在表內,實測 a{2} 出來是 a\{2\};而 -、/、空格這三樣不在表內,a-b、a/b、a b 進去都原樣出來。
替換詞統一填 X、另兩個開關關著,同一串尋找詞兩檔各跑一次:
| 尋找詞 | 輸入文字 | 字面檔 | 正則檔 |
|---|---|---|---|
a.b | a.b aXb Y.b a.b | 2 | 3 |
a(b) | a(b) a(b)x ab | 2 | 1 |
a{2} | aa a{2} aaa | 1 | 2 |
a|b | a|b a b c | 1 | 4 |
a?b | a?b ab b | 1 | 3 |
字面檔只認逐字元相同的那兩處 a.b,輸出 X aXb Y.b X;正則檔的點吃任意字元,連 aXb 也算命中。差得最狠的是 a|b,1 對 4:豎線在字面檔只是串裡的一個符號,在正則檔成了或運算。
區分大小寫只增刪 flags 裡的一個字母
模板逐字寫的是 caseSensitive ? 'g' : 'gi':不勾(預設)拿到帶 i 的 gi,勾上才是 g。輸入 Hello hello HELLO heLLo、尋找 hello、替換 X:不勾報 4,原始字元 23 變結果字元 7;勾上報 1,只有純小寫那個被換掉,23 變 19,兩檔差 3 次。
「僅替換第一個匹配」把 g 摘掉,順手換了尺子
這一檔做兩件事:先用 flags = flags.replace('g', '') 把 g 摘掉,讓替換只落在第一處;計數分支則從「命中幾條」換成比較替換前後整串——if (before !== result) 才加一,於是每條規則在這一檔最多貢獻 1,加不加看的是這串變沒變。
圖 1:同一批樣本下「替換次數」的讀數與真實改動處數
零寬匹配:統計那一格與落地那一格分家
非 firstOnly 檔的那個數不來自替換的返回值:先用 pattern.source 帶 g 重建一個正則去 match() 取長度,再另跑一次替換落地,兩個物件互不攜帶狀態。命中長度為零的正則,從這裡開始和「改了幾處」分道:
x*於abc(串裡沒有 x)統計 4 次;替換詞填X得XaXbXcX,結果字元從 3 漲到 7;填空字串則輸出abc一字不差,那一格仍報 4。\b於a bc統計 4 次,得XaX XbcX,4 漲到 8。(?=a)於aba統計 2 次,得XabXa,3 漲到 5。- 錨點
^與$於abc各統計 1 次,各只多插 1 個字元。 a*於baa統計 3 次,替換詞 A 得AbAA,3 變 4。
空位為什麼這麼多:帶 g 的匹配遇到零寬命中時靠 lastIndex 加一強制推進,/x*/g 打在 abx 上能收出 4 項(三個空字串加一個 x)。這一檔裡統計次數與替換實際走的編輯次數逐例相等,真正對不上的是它與結果字元增減。
空尋找詞整條丟棄,壞正則一聲不響
規則收集那行是 if (inputs[0].value):尋找框留空的規則在收集階段就被整條丟掉,「規則數量」顯示的是篩選後的條數,不等於螢幕上的行數。三行裡第 2 行尋找留空,規則數量報 2、與行數差 1;尋找填 0 屬真值字串照樣保留;三行尋找全空則規則數量 0、替換次數 0、輸出原樣。
編譯不過的正則走的是另一條:new RegExp 拋錯被 catch (e) {} 這個空分支吞掉,沒有提示文案,那條規則進了清單卻沒執行。三條規則 a→X、b→Y、一個左括號→Z 打在 a b c 上,第三條拋錯,輸出 X Y c 保留前兩條結果、替換次數 2、規則數量照報 3;同樣這三串在字面檔會被轉義成字面值、不拋錯,次數變 3。唯一的線索就是次數比預期少。
「原始字元 / 結果字元」數的是 UTF-16 碼元
那兩格取的是 raw.length 與 result.length,既不是碼點也不是位元組:一枚 emoji 報 2,一個漢字「中」報 1,一個增補平面漢字(U+20000)報 2;中文測試 報 4,UTF-8 編碼是 12 位元組。首屏預置的三行 Hello 是純 ASCII,43 碼元、43 位元組,init() 塞的那條 Hello→Hi 跑完輸出三行 Hi、結果字元 34——每處省 3 碼元,三處共省 9。
規則行那個屬性值是怎麼拼出來的
addRule(find, replace) 產生規則行時把尋找詞直接拼進 HTML 屬性,第 71 行逐字是 'value="' + (find || '') + '" oninput="process()"',兩側沒有做引號處理。字串樁裡逐字比對拼出的片段與屬性讀回的值:尋找詞 Hello 拼出 value="Hello"、原樣讀回;含單引號的 a'b 拼出 value="a'b",單引號在雙引號屬性裡不需要轉義,也原樣讀回;含兩個裸雙引號的 say "hi" now 拼出的片段逐字是 value="say "hi" now",而屬性讀取從 value=" 起讀到下一個裸雙引號為止,實際讀回停在 say ,後面那截落到屬性之外。替換詞那一格用的是同一行寫法。
FAQ
Q:替換次數能當成「改了多少處」嗎?
不能直接用。非 firstOnly 檔它等於各條規則命中數的累加,零寬空位也進帳;firstOnly 檔換成整串變沒變,每條最多 1。同一份輸入兩檔讀數是 5 和 1,自等替換時一個報 5、一個報 0。
Q:勾上正則後命中變多,是哪裡壞了?
不是。字面檔走那張點名 14 個元字元的轉義表,正則檔把同一串按語法解釋,撞上的位置本來就不同。想先確認寫法本身,另一篇記過單獨驗一遍的路子,這裡不重複。
Q:統計區整塊不見了,是不是沒跑?
輸入為空時四格連標籤一起被清空,那是分支本身的行為;輸入一串空格則照常顯示。數字看著不對,先分辨它屬於哪一檔口徑,再看有沒有哪一行的尋找框留空。