雜記 › 技術筆記 › 刪右側給空串、截右側還整行:text-line-ops 的碼元口徑與越界不對稱實測

刪右側給空串、截右側還整行:text-line-ops 的碼元口徑與越界不對稱實測

後台名單要統一去掉行首 2 個字元,夾生僻字那行卻只剩半個方框。把模板腳本逐字切出來在 Node 裡原樣跑過:substring 數的是 UTF-16 碼元,越界時刪除檔給空串、截取檔還整行,min 屬性全程未被任何計算引用。

刪掉每行開頭 2 個字元,有一行開頭變成方框

一份後台匯出的名單,一行一項,要統一去掉行首的 2 個字元。多數行都乾淨,唯獨夾了生僻字那一行,行首變成一個認不出的方框帶著剩下的字;同一批裡帶 emoji 的那列也一樣,刪 1 個「字元」像刪了半個。於是把那頁的腳本逐字切出來在 Node 裡原樣跑了一遍:模板實測 8018 位元組、175 行,唯一的腳本段 2708 個 UTF-16 碼元、3176 位元組,刪除與截取兩檔的行為全壓在同一句 substring 上。

它數的是 UTF-16 碼元,不是字形

文本行操作的 process() 四個分支都走 String.prototype.substring,單位是 UTF-16 碼元。樣本賬逐字量過:

  • "苹" 佔 1 個碼元、UTF-8 裡 3 位元組,預置樣本那五種水果名全在這一類;
  • 增補平面那個生僻字在 U+20000,是代理對 \uD840\uDC00,佔 2 個碼元、UTF-8 4 位元組,十六進位 f0a08080;
  • 一枚 emoji 同樣 2 個碼元、4 位元組;
  • 拼串:這個生僻字打頭接「甲乙」、或接在「甲乙」之後,長度都是 4;emoji 拼 abc 兩頭都是 5。

n 填 1 會切壞半個代理對

刪除檔取左側、n 填 1 打那串「生僻字加甲乙」,輸出是 3 個碼元的串,開頭是個孤立低代理,探針原文寫作 "\udc00甲乙";同一串給截取檔左側 n=1,只截到代理對的左半邊,吐出 1 個碼元的 \ud840。右側同構:刪右 1 留下行尾的孤立高代理,截右 1 得 \uDC00;emoji 那串刪左 1 得到開頭是 \ude00 的 4 碼元串。n 填到 2 才正好吃掉整個代理對。孤立代理在 JS 裡仍是合法碼元,length 照算 1,可任何一次 UTF-8 編碼都會把它換成 U+FFFD:實測 Buffer.from("\uD840", "utf8") 得 3 位元組 efbfbd,低代理那半邊同樣。

越界時刪右側給空串、截右側還給整行

四個分支裡只有一處寫了顯式的越界判斷:模板 L138 的刪除右,形如 n >= line.length ? "" : line.substring(0, line.length - n)。其餘三檔(L136 刪除左、L144 截取左、L146 截取右)全靠 substring 自己的鉗位兜底,起點為負按 0、終點超長按行長。

同一份三行輸入、同一個 n:刪除清空,截取把短行原樣吐回 輸入行 刪除左 刪除右 截取左 截取右 ABCDE E A ABCD BCDE AB 空串 空串 AB AB 整行 A 空串 空串 A A 整行 差別只在原始碼有沒有寫顯式判斷,其餘三檔由 substring 鉗位

圖 1:三行長度不等的輸入在同一個 n 下的四檔實測

想「只留後幾個字元」的人,對短行拿到的是全文,一個字元都沒少;刪除檔在想刪乾淨時反倒是安全的。

兩個 n 的預設值都是 2,min 一次都沒參與

模板 L34 與 L44 逐字對讀:兩個 number 輸入框的 value 寫的的都是 2,差別只在 min,刪除檔 min="0"、截取檔 min="1"——「截取檔預設少刪一個」是把 min 當成了預設值。整段腳本裡 min、max、step、validity、checkValidity 這五個標識符出現次數合計 0,process() 從不查詢校驗狀態。首屏就能驗證:預置樣本 5 行、每行恰好 2 個碼元,方向取兩個 select 的首個選項 left,substring(2) 把每行刪空,輸出實測是 4 個換行碼元加 5 個空行,統計照報「處理行數:5」。

parseInt 後面掛個 ||0,攔不住負數也攔不住字母

兩條 n 輸入框的讀取語句寫成 parseInt(..., 10) || 0,而 || 0 只擋 NaN 與 0。下表左列是逐字餵進 value 的字串,後面是歸一化結果與單行輸入 "ABCD"(4 個碼元)在四檔的實測:

填進去的歸一化成刪除左刪除右截取左截取右
空串0ABCDABCD清空清空
"abc"0ABCDABCD清空清空
"2.7"2,截到小數點前CDABABCD
"1e3"1,遇到字母 e 即停,不是 1000BCDABCAD
"-3"-3,負數原樣通過ABCDABCD清空清空
20 個 9 連寫1e20清空清空ABCDABCD

尾換行讓統計多報一行,空行永遠進結果

原始碼 L127 是 input.split("\n"),迴圈體對每個元素無差別執行分支再 push,沒有一處跳過空字串行,而 L167 那個「處理行數」直接取 lines.length。五組對賬(除註明外都是刪除左 n=1):

  • 五行樣本不帶尾換行:split 出 5 段,報 5,與內容一致;
  • 「蘋果\n香蕉\n」末尾多 1 個換行:split 出 3 段,報 3,真實內容只有 2 行,差 1;
  • 空輸入:split 得 1 段,報 1,內容 0 行;
  • 「x\ny\n\n」兩行內容再拖 2 個空尾行:報 4,內容 2,差 2;
  • 「a\n\nb」的中間空行原樣進結果,行位置一一對應;截取右 n=3 打「hello\n\nworld」也是 3 行、中間那行空著。

行數對不上並不說明有行被刪空,先清掉空行再做行級處理更省事,思路見 清理文本空行與空白。

切檔不重置參數,殘留值還在自己的檔裡等

switchMode 只改寫當前檔變數、換按鈕的 active 類、給四個參數區的 div 逐個寫 style.display、刷新提示文案,最後調一次 process();全函式沒有一行給輸入控件賦值。探針給每個元素記 value 寫入次數:連點 6 次繞完四個 tab 再切回來,前後綴與序號那兩檔的 5 個輸入框寫入次數全是 0——參數 DOM 常駐,只是被 display 藏起來。那兩檔各自怎麼填,是 加前綴與加序號那篇 的主題,本篇不碰。

殘留值歸誰:輸入 "ABCDEFGH"(8 個碼元,方向顯式設 left),刪除檔把 del-n 改成 5,輸出 "FGH";切到截取檔,它讀的是自己的 cut-n 初值 2,輸出 "AB",那個 5 並沒有串檔。

FAQ

Q:刪 1 個字元為什麼會弄壞沒動的字?

substring 數的是 UTF-16 碼元,增補平面漢字與 emoji 各佔 2 個,n 填 1 只切走半邊;剩下的孤立代理再過一次 UTF-8 編碼就變成 3 位元組的 efbfbd,原字拼不回來。

Q:想只留後 n 個字元,短行為什麼沒被裁?

截取右側在 n 大於行長時起點為負、按 0 處理,整行原樣返回;同樣長度打刪除檔則是空串。要「短行留不住就清空」,得自己先把短行挑出來。

Q:n 裡填負數或字母,頁面為什麼不彈提示?

讀取語句只有 parseInt(..., 10) || 0,空串與字母歸成 0,負數原樣通過;min 在腳本裡出現 0 次,不參與任何判斷。刪除檔對負數是無操作,截取檔則是清空,兩檔都不彈提示。

📎 相關文章

替換次數報 5 還是報 1:批次尋找替換的三個開關與零寬匹配實測 2026-11-07 空一個框就 0 張卡:分數通分頁的靜默失敗、跳號的「分數N」與 990 行不約分 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