一、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 z | 3 | 差值(第 130 行) |
| 僅顯示重複項 | x y | 2 | 等於結果行數(第 154 行) |
| 僅顯示唯一項 | z | 5 | 差值(第 164 行) |
換成 7 行的 apple、Banana、apple、cherry、Banana、apple、date,三個數照舊是 3、2、5:問「被刪掉幾行」只有保留首次/保留末次對得上,僅顯示重複項報的是重複值的種類數。總行數是第 114 行去除空行之後的 lines.length,不是輸入框原始行數;選「不去重」時第三項不呈現(第 192 行)。
七、沒有上限,只有每次按鍵全量重算
範本 198 行 / 7897 位元組,沒有長度上限也沒有格式白名單:單行 1000000 字元照常輸出 1000000 位元組。100000 行、669999 位元組的輸入在 Node v24.19 實測:不去重 68 ms、升序 710 ms、升序加保留首次 273 ms,兩兩 localeCompare 比不排序慢約 10 倍;輸入框每次按鍵全量重算(第 53 行 oninput),大名單的體感是打字變卡而非報錯。
八、隨機打亂沒毛病,毛病是不可復現
它是標準 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 兩個數。