雜記 › 技術筆記 › 替換次數報 5 還是報 1:批次尋找替換的三個開關與零寬匹配實測

替換次數報 5 還是報 1:批次尋找替換的三個開關與零寬匹配實測

批次尋找替換的三個開關各自改了哪一段:字面檔轉義表點名 14 個元字元,摘掉 g 後替換次數改看整串變沒變,零寬匹配統計 4 次而輸出可以一字不差;空尋找詞整條丟棄,壞正則靜默跳過。

同一條規則,「替換次數」這一格有兩套量法

輸入 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.ba.b aXb Y.b a.b23
a(b)a(b) a(b)x ab21
a{2}aa a{2} aaa12
a|ba|b a b c14
a?ba?b ab b13

字面檔只認逐字元相同的那兩處 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,加不加看的是這串變沒變。

深色是這一格報的次數,淺色是真實改動的處數 cat→dog 全量檔 報 5 / 改 5 cat→dog 僅第一個匹配 報 1 / 改 1 cat→cat 自等替換 全量報 5 / 僅第一個報 0 真實改動 0 處

圖 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:統計區整塊不見了,是不是沒跑?

輸入為空時四格連標籤一起被清空,那是分支本身的行為;輸入一串空格則照常顯示。數字看著不對,先分辨它屬於哪一檔口徑,再看有沒有哪一行的尋找框留空。

📎 相關文章

空一個框就 0 張卡:分數通分頁的靜默失敗、跳號的「分數N」與 990 行不約分 2026-11-07 刪右側給空串、截右側還整行:text-line-ops 的碼元口徑與越界不對稱實測 2026-11-07 120 個詞槽、15 條整句、1 句寫死的開頭:lorem-ipsum 的詞表清點與重複片段實測 2026-11-05 SVG 預覽的尺寸到底聽誰的:顯示框恆等於容器,刪掉 viewBox 之後 r=1 實測 2 像素 2026-11-05 文字分列後空欄位沒丟:a,,b 照留、列號越界填空、填 0 或 abc 退回全部列實測 2026-11-05