🌐用 JavaScript 實作多欄位排序:先依 SEQ_NO,再依 APS_PLAN_NO,最後依 TFT4(空值排最後)
- 取得連結
- X
- 以電子郵件傳送
- 其他應用程式
摘要
教你用 JavaScript 為資料列實作「先依 SEQ_NO、再依 APS_PLAN_NO、最後依 TFT4(空值排最後)」的多欄位排序。包含完整程式碼、逐行說明、常見陷阱與可重用的排序工具。
為什麼需要多欄位排序?
實務上,我們常需要 先用主要欄位 排序(例如工單次序 SEQ_NO),若相同再用 次要欄位(APS_PLAN_NO),最後再用 第三順位欄位(TFT4)。更麻煩的是,TFT4 可能為 null、空字串或只有空白——我們希望這些 空值排在最後,避免干擾有效值的排序。
本篇提供一段 可直接套用 的 JavaScript 排序程式,並解釋每個細節與容易踩到的坑。
最終目標的排序規則
-
第一順位:
SEQ_NO -
第二順位:
APS_PLAN_NO -
第三順位:
TFT4(空值排最後,其餘照字面自然排序)
「空值」包含 null、undefined、空字串 ''、或只有空白字元的字串。
直接可用的排序程式碼(新版)
// 需求:先依 SEQ_NO,再依 APS_PLAN_NO,最後依 TFT4(空值排最後)
// 建立單一比較子:一般字串/數字混合 → 自然排序
const collator = new Intl.Collator(undefined, { numeric: true, sensitivity: 'base' })
const norm = (v) => (v ?? '').toString().trim()
// 建立「空值排最後」的欄位比較子(專供 TFT4 用)
const cmpEmptyLast = (x, y) => {
const ax = norm(x)
const by = norm(y)
const axBlank = ax === ''
const byBlank = by === ''
if (axBlank && byBlank) return 0
if (axBlank) return 1 // x 空 → 往後排
if (byBlank) return -1 // y 空 → 往後排
return collator.compare(ax, by)
}
// 以「比較子合成」的方式組裝多欄位排序規則
const compareBy = (...fns) => (a, b) => {
for (const fn of fns) {
const r = fn(a, b)
if (r !== 0) return r
}
return 0
}
// 三段式規則:SEQ_NO → APS_PLAN_NO → TFT4(空值最後)
rows.sort(
compareBy(
(a, b) => collator.compare(norm(a.SEQ_NO), norm(b.SEQ_NO)),
(a, b) => collator.compare(norm(a.APS_PLAN_NO),norm(b.APS_PLAN_NO)),
(a, b) => cmpEmptyLast(a.TFT4, b.TFT4)
)
)
為什麼這樣寫?
-
使用 比較子合成(compareBy) 讓規則一目了然、易擴充。
-
Intl.Collator以 自然排序 處理「字串中的數字」(例如"2"<"10")。 -
cmpEmptyLast先判斷是否為空,再進行文字比較,可讀性高且容易單測。
範例資料與排序前後對照
const rows = [
{ SEQ_NO: '2', APS_PLAN_NO: 'A100', TFT4: 'T2' },
{ SEQ_NO: '2', APS_PLAN_NO: 'A100', TFT4: ' ' }, // 空白視為空值
{ SEQ_NO: '1', APS_PLAN_NO: 'A200', TFT4: 'T1' },
{ SEQ_NO: '1', APS_PLAN_NO: 'A100', TFT4: 'T3' },
{ SEQ_NO: '1', APS_PLAN_NO: 'A100', TFT4: null }, // null 視為空值
]
// 套用上方的 sort 比較器後:
// 1) 先比 SEQ_NO('1' 群組在 '2' 前)
// 2) 在 SEQ_NO 相同時比 APS_PLAN_NO(A100 在 A200 前)
// 3) 在同群組內比 TFT4,空值最後
rows.sort(
compareBy(
(a, b) => collator.compare(norm(a.SEQ_NO), norm(b.SEQ_NO)),
(a, b) => collator.compare(norm(a.APS_PLAN_NO),norm(b.APS_PLAN_NO)),
(a, b) => cmpEmptyLast(a.TFT4, b.TFT4)
)
)
可重用的「多欄位排序工具」(DSL 風格)
// 以鏈式 DSL 建立多欄位比較器
function order() {
const coll = new Intl.Collator(undefined, { numeric: true, sensitivity: 'base' })
const steps = []
const toS = (v) => (v ?? '').toString().trim()
return {
by(key) {
steps.push((a, b) => coll.compare(toS(a[key]), toS(b[key])))
return this
},
byEmptyLast(key) {
steps.push((a, b) => {
const ax = toS(a[key]); const bx = toS(b[key])
const ae = ax === ''; const be = bx === ''
if (ae || be) return ae === be ? 0 : (ae ? 1 : -1)
return coll.compare(ax, bx)
})
return this
},
build() {
return (a, b) => {
for (const s of steps) {
const r = s(a, b)
if (r) return r
}
return 0
}
}
}
}
// 使用方式
const cmp = order()
.by('SEQ_NO')
.by('APS_PLAN_NO')
.byEmptyLast('TFT4')
.build()
rows.sort(cmp)
好處:
-
規則宣告式、可讀性高。
-
之後想改成
byDesc、或加入更多欄位,不必重寫整個比較器。
進階:使用 Intl.Collator 與「空值權重」優化
const coll = new Intl.Collator(undefined, { numeric: true, sensitivity: 'base' })
const T = (v) => (v ?? '').toString().trim()
rows.sort((a, b) => {
// 1) SEQ_NO
let r = coll.compare(T(a.SEQ_NO), T(b.SEQ_NO))
if (r) return r
// 2) APS_PLAN_NO
r = coll.compare(T(a.APS_PLAN_NO), T(b.APS_PLAN_NO))
if (r) return r
// 3) TFT4(先比空值權重,再比字面)
const aw = T(a.TFT4) === '' ? 1 : 0
const bw = T(b.TFT4) === '' ? 1 : 0
if (aw !== bw) return aw - bw // 非空(0) 先於 空(1)
return coll.compare(T(a.TFT4), T(b.TFT4))
})
此寫法把「空值最後」拆成 權重比較(數字)+ 字面比較(文字),在大資料量下仍具備良好可讀性與效能。
常見陷阱與最佳實務
-
字串 vs. 數字
後端若傳回混合型態(有時數字、有時字串),比較前先String()正規化,並用Intl.Collator的numeric: true避免"10" < "2"的字典序陷阱。 -
空白字元
trim()可避免" "被誤判為有效值。 -
穩定排序(Stable Sort)
現代主流 JS 引擎的Array.prototype.sort已採用穩定排序,但若需 極舊環境 仍應驗證或引入 polyfill。 -
僅對需要的欄位做空值處理
本例僅對TFT4做「空值最後」,其它欄位維持一般排序,以符合業務邏輯。 -
效能
-
重複排序時,重用同一個
Intl.Collator。 -
資料量極大(>10 萬)時,可考慮先分群再排序,降低比較次數。
-
單元測試建議(以 Node.js assert 範例)
import assert from 'node:assert/strict'
const coll = new Intl.Collator(undefined, { numeric: true, sensitivity: 'base' })
const toS = (v) => (v ?? '').toString().trim()
const cmpEmptyLast = (x, y) => {
const ax = toS(x), by = toS(y)
const ae = ax === '', be = by === ''
if (ae || be) return ae === be ? 0 : (ae ? 1 : -1)
return coll.compare(ax, by)
}
const compareBy = (...fns) => (a, b) => {
for (const fn of fns) {
const r = fn(a, b)
if (r) return r
}
return 0
}
const data = [
{ SEQ_NO: '2', APS_PLAN_NO: 'A100', TFT4: '' },
{ SEQ_NO: '1', APS_PLAN_NO: 'A100', TFT4: 'T3' },
{ SEQ_NO: '1', APS_PLAN_NO: 'A100', TFT4: null },
{ SEQ_NO: '1', APS_PLAN_NO: 'A200', TFT4: 'T1' },
]
const sorted = [...data].sort(
compareBy(
(a, b) => coll.compare(toS(a.SEQ_NO), toS(b.SEQ_NO)),
(a, b) => coll.compare(toS(a.APS_PLAN_NO), toS(b.APS_PLAN_NO)),
(a, b) => cmpEmptyLast(a.TFT4, b.TFT4)
)
)
// 驗證順序(以索引或欄位檢查)
assert.equal(sorted[0].SEQ_NO, '1')
assert.equal(sorted[1].SEQ_NO, '1')
assert.equal(sorted[2].SEQ_NO, '1')
assert.equal(sorted[3].SEQ_NO, '2')
assert.equal(sorted[0].APS_PLAN_NO, 'A100')
assert.equal(sorted[1].APS_PLAN_NO, 'A100')
assert.equal(sorted[2].APS_PLAN_NO, 'A200')
assert.equal(sorted[0].TFT4, 'T3') // 非空先於空
assert.equal(sorted[1].TFT4, null)
提示:也可用 Vitest/Jest 撰寫更語意化的斷言(例如 expect(sorted).toEqual([...]))。
結語
本文示範了如何在前端以 清楚、可維護、可測試 的方式,完成「先 SEQ_NO → 再 APS_PLAN_NO → 最後 TFT4(空值排最後)」的排序需求。你可以直接複製貼上使用,或採用通用比較器在專案中重複利用。
需要把它整合進 Vue 的表格或匯出 Excel 的流程嗎?把你的欄位定義與資料量告訴我,我可以幫你做最佳化與單元測試範本。
- 取得連結
- X
- 以電子郵件傳送
- 其他應用程式
留言
張貼留言