← 中文资源

简体中文版本

多键稳定排序

按多个键函数与升降序排序记录,并用原始下标兜底,保证相等元素不交换位置。

阅读英文原文

功能说明

该示例支持按多个字段依次排序,例如先按状态升序、再按更新时间降序、最后按名称升序。每个排序键可以是取值函数,也可以是取值函数与方向的二元组。实现采用装饰排序再还原的方式,为每个元素记录原始下标,当所有键都比较相等时用下标决定顺序,因此在任何运行时都能得到稳定结果。返回新数组,不修改输入。

关键代码

compareValues 统一了空值、数字、日期与字符串的比较规则:null 与 undefined 排在最后;数字使用差值并显式处理 NaN;日期按时间戳;其余值转成字符串后按码点比较,避免 localeCompare 带来的环境差异。stableSortBy 先规范化选择器并校验方向,再生成带下标的包装对象,排序时逐键比较,方向为降序时对结果取反,最后仅保留原值。

使用注意

字符串比较是二进制的,不会按中文拼音或地区规则排序;需要本地化排序时应在比较函数中显式使用 Intl.Collator。空值排序规则固定为最后,且降序时取反会让空值排到最前,调用方若需要“空值始终最后”应拆分处理。选择器每次比较都会被调用,计算成本高的键应预先映射成临时字段。稳定排序保证相等元素相对顺序不变,但不会去重。

验证方式

构造状态相同、时间不同的记录,按两个键排序并断言顺序;添加若干完全相等的记录,断言输出保持输入相对顺序;测试降序方向与空值位置;使用 Date 键与 NaN 数字键验证比较结果;传入空选择器数组、非法方向与非函数选择器,分别断言 TypeError 或 RangeError;确认原数组未被修改。

代码示例

function compareValues(left, right) {
  if (left === right || Object.is(left, right)) return 0;
  if (left === null || left === undefined) return 1;
  if (right === null || right === undefined) return -1;
  if (typeof left === "number" && typeof right === "number") {
    if (Number.isNaN(left)) return Number.isNaN(right) ? 0 : 1;
    if (Number.isNaN(right)) return -1;
    return left - right;
  }
  if (left instanceof Date && right instanceof Date) {
    return left.getTime() - right.getTime();
  }
  const leftText = String(left);
  const rightText = String(right);
  if (leftText === rightText) return 0;
  return leftText < rightText ? -1 : 1;
}

export function stableSortBy(items, selectors) {
  if (!Array.isArray(items)) throw new TypeError("items must be an array");
  if (!Array.isArray(selectors) || selectors.length === 0) {
    throw new TypeError("selectors must be a non-empty array");
  }
  const keys = selectors.map((entry) => {
    if (typeof entry === "function") return { select: entry, direction: "asc" };
    if (Array.isArray(entry) && typeof entry[0] === "function") {
      const direction = entry[1] ?? "asc";
      if (direction !== "asc" && direction !== "desc") {
        throw new RangeError("direction must be asc or desc");
      }
      return { select: entry[0], direction };
    }
    throw new TypeError("selector must be a function or [function, direction]");
  });

  return items
    .map((value, index) => ({ value, index }))
    .sort((left, right) => {
      for (const { select, direction } of keys) {
        const result = compareValues(select(left.value), select(right.value));
        if (result !== 0) return direction === "desc" ? -result : result;
      }
      return left.index - right.index;
    })
    .map((entry) => entry.value);
}