雜記 › 技術筆記 › 升序按的不是碼點:localeCompare('zh') 的拼音序與 3 種重複行數分母

升序按的不是碼點:localeCompare('zh') 的拼音序與 3 種重複行數分母

一、6 行名單,排完兩邊都對不上

名單只有 6 行:蘋果、香蕉、橘子、安卓、北京、上海。想按升序排一遍,拿到的是 安卓 北京 橘子 蘋果 上海 香蕉。這既不是我心裡按碼點算出來的 上海 北京 安卓 橘子 蘋果 香蕉,也不是筆畫序——它是拼音序,ān běi jú píng shàng xiāng。

這份名單我最後是在文本排序去重裡跑的,口徑寫死在範本第 170 行:比較器是 a.localeCompare(b, 'zh'),走瀏覽器 ICU 的中文 collation。文案那句「按 Unicode 編碼從小到大」不成立,「中文最後」也站不住:蘋果、zebra、Apple、香蕉 放同一列,升序給出 蘋果 香蕉 Apple zebra,漢字整塊排在拉丁字母前面,而碼點序應是 Apple zebra 蘋果 香蕉。

它也沒排抖:重排 300 次結果 300/300 全等,兩兩比較的 16 格沒有傳遞性矛盾——是個穩定但不同於碼點序的全序。

二、item10 仍然排在 item2 前面

帶編號的名單最容易起爭議。item10、item2、item1、item21、item100 升序得到 item1 item10 item100 item2 item21。純數字也按字元位:10、9、100、2、0 排成 0 10 100 2 9。

第 170 行沒傳 {numeric:true},也沒有 sensitivity 和 caseFirst。表格裡習慣看到 item2 排在 item10 前,這裡不是排錯,是自然序那一檔沒開,想要只能自己補零。

大小寫的分工更微妙:banana、Banana、apple、Apple、cherry 勾不勾「區分大小寫」,升序都是 apple Apple banana Banana cherry;去重一開,Apple、apple、APPLE、banana、Banana 取消勾選只剩 2 行、重複行數 3。第 122 行的 toLowerCase() 只進去重的鍵,不進第 170 行的比較器。

三、「按長度」數的是碼元,不是字形也不是位元組

第 181 行的 a.length - b.length 數的是 UTF-16 碼元:ab、一個 4 位元組的 emoji、a、蘋果、abc、é 按長度升序得 a(1) ab(2) emoji(2) 蘋果(2) é(2) abc(3)——1 個字形的 emoji 與 2 位元組的 ab 等長,6 位元組的「蘋果」也等長,等長的行沒有次級比較器。

四、去重認的是碼點全等,而且不 trim

apple、帶尾空格的 apple、帶尾 Tab 的 apple、帶前空格的 Apple,再加 1 行空行:5 行選保留首次,結果行數 5、重複行數 0。判「相同」用的是碼點全等,首尾空白一個都不抹。

空行卻由 trim 決定:a、空行、半形空格、U+3000、Tab、b 這 6 行,開啟「去除空行」只剩 2 行,總行數也從 6 變成 2。切行只認 \n。Windows 貼上安全,textarea 已把 CRLF 歸一成 LF:4 行 apple、banana、apple、cherry 實測 總行數 4、結果行數 3、重複行數 1。

五、行名撞上 constructor 這種詞,整行會消失

唯一會讓資料靜默蒸發的是行名。9 行輸入:apple、constructor、toString、valueOf、hasOwnProperty、isPrototypeOf、__proto__、propertyIsEnumerable、banana,選保留首次,輸出只剩 apple 和 banana 兩行,統計卻是 總行數 9、結果行數 2、重複行數 7。

根因在第 120–121 行的 const seen = {} 與 const duplicates = {}:普通物件字面量讀鍵會順原型鏈上去,第 128 行 if (!seen[k]) 對其中 6 個名字恆為假,__proto__ 讀到 Object.prototype 也是真值。7 行全是這類名字時輸出 0 行;4 行 constructor、constructor、toString、toString 選「僅顯示重複項」輸出 0 行、重複行數報 0。欄位名清單和翻譯 key 表最容易撞上;兩份名單求交集是另一個問題。

六、同一段 x、x、x、y、y、z,重複行數有三個數

去重模式輸出重複行數式子
保留首次 / 保留末次x y z3差值(第 130 行)
僅顯示重複項x y2等於結果行數(第 154 行)
僅顯示唯一項z5差值(第 164 行)

換成 7 行的 apple、Banana、apple、cherry、Banana、apple、date,三個數照舊是 3、2、5:問「被刪掉幾行」只有保留首次/保留末次對得上,僅顯示重複項報的是重複值的種類數。總行數是第 114 行去除空行之後的 lines.length,不是輸入框原始行數;選「不去重」時第三項不呈現(第 192 行)。

重複行數:同一段 x,x,x,y,y,z 保留首次 3 僅顯示重複項 2 僅顯示唯一項 5 圖 1:三種去重模式的重複行數分母

七、沒有上限,只有每次按鍵全量重算

範本 198 行 / 7897 位元組,沒有長度上限也沒有格式白名單:單行 1000000 字元照常輸出 1000000 位元組。100000 行、669999 位元組的輸入在 Node v24.19 實測:不去重 68 ms、升序 710 ms、升序加保留首次 273 ms,兩兩 localeCompare 比不排序慢約 10 倍;輸入框每次按鍵全量重算(第 53 行 oninput),大名單的體感是打字變卡而非報錯。

耗時 ms:同一份 100000 行輸入 不去重 68 升序加保留首次 273 升序 710 圖 2:排序把這一步的耗時放大

八、隨機打亂沒毛病,毛病是不可復現

它是標準 Fisher-Yates(第 176–179 行):4 元素跑 2400 次,24 種排列全部出現,最少 84 次、最多 123 次(期望 100 次),分佈沒缺陷;麻煩在順序取自 Math.random,兩次點擊不同也復現不回來,演算法取捨在打亂名單那篇寫過。

Q:想要碼點序或者筆畫序,能選嗎?

不能。第 170 行的 locale 寫死成 'zh',沒有拼音/碼點/自然序的口徑選擇,要 item2 在前只能補零。

Q:匯出的行數比輸入少,先查哪一處?

三處:空行被 trim 判掉(U+3000 和 Tab 也算空行);取消勾選「區分大小寫」後 Apple 與 apple 合併;行名撞上 constructor 這類原型屬性名時整行被判成重複——前兩種是口徑,最後一種是資料沒了。

Q:報表要引用哪個重複行數?

問「刪掉幾行」用保留首次或保留末次(第 130 行的總行數減結果行數);問「幾種值重複」用僅顯示重複項,它等於結果行數(第 154 行)。同一段輸入裡是 3 和 2 兩個數。

📎 相關文章

文本合併後行數對不上:缺尾末換行、被吃掉的 BOM 和不可靠的順序 2026-10-26 Excel 貼上轉 JSON:日期、長數字與精度在貼上那一刻就已經變了 2026-10-26 條碼解碼只吐原文:EAN-13 檢查碼、UPC-A 的前導零與 CODE128 的模 103 2026-10-26 二維碼解碼實測:糾錯等級救不了遮擋,每模組 2 像素才是生死線 2026-10-26 字頻還是詞頻:同一句中文,一份 23 項榜單和一份空表 2026-10-26