
理解复杂数组重排序问题
在前端开发中,我们经常会遇到需要根据特定逻辑对数据数组进行重排的需求。一个常见的场景是处理具有父子关系的数据结构,其中每个元素可能包含一个id和一个reference_id(或parent_id),reference_id指向其父元素的id。我们的目标是将所有子元素紧跟在它们的父元素之后,即使父元素本身没有reference_id(即它是顶级元素)。
考虑以下数据结构:
const initialArray = [
{ id: 1, name: 'hello world', reference_id: null },
{ id: 2, name: 'hello world', reference_id: null },
{ id: 3, name: 'hello world', reference_id: 1 },
{ id: 4, name: 'hello world', reference_id: null },
{ id: 5, name: 'hello world', reference_id: 1 },
{ id: 6, name: 'hello world', reference_id: 2 },
];我们期望的排序结果是:
[
{ id: 1, name: 'hello world', reference_id: null },
{ id: 3, name: 'hello world', reference_id: 1 },
{ id: 5, name: 'hello world', reference_id: 1 },
{ id: 2, name: 'hello world', reference_id: null },
{ id: 6, name: 'hello world', reference_id: 2 },
{ id: 4, name: 'hello world', reference_id: null },
]可以看到,id: 3和id: 5(它们的reference_id都是1)被排在了id: 1之后;id: 6(reference_id是2)被排在了id: 2之后。
直接使用 findIndex 和 splice 等方法来动态修改数组通常会导致复杂且难以维护的代码,尤其是在循环中修改数组长度和索引时,容易产生“意外结果”或逻辑错误。更推荐的做法是利用数组的 sort 方法,结合自定义的比较函数。
立即学习“Java免费学习笔记(深入)”;
解决方案一:两步映射与排序
这种方法的核心思想是:首先为每个元素创建一个临时的排序键,然后根据这个键进行排序,最后移除这个临时键,还原为原始对象。
实现步骤
- 创建排序键: 遍历原始数组,为每个元素生成一个包含原始数据和排序键的新对象。排序键通常由reference_id和id组合而成。
- 执行排序: 使用 Array.prototype.sort() 方法,根据新对象中的排序键进行比较。
- 还原数据: 再次遍历排序后的数组,提取出原始数据对象。
示例代码
const arr = [
{ id: 1, name: 'hello world', reference_id: null },
{ id: 2, name: 'hello world', reference_id: null },
{ id: 3, name: 'hello world', reference_id: 1 },
{ id: 4, name: 'hello world', reference_id: null },
{ id: 5, name: 'hello world', reference_id: 1 },
{ id: 6, name: 'hello world', reference_id: 2 },
];
const sortedResult = arr
.map(item => ({
// 构建排序键:将 reference_id 视为父ID,如果为 null 则视为空字符串,
// 然后拼接当前 id。这样,父元素(reference_id为null)的键会以其id开头,
// 子元素(reference_id为父id)的键会以父id开头。
// 例如:
// id: 1, ref: null -> "1"
// id: 3, ref: 1 -> "13"
// id: 5, ref: 1 -> "15"
// id: 2, ref: null -> "2"
// id: 6, ref: 2 -> "26"
sortKey: `${item.reference_id ?? ''}${item.id}`,
originalElement: item,
}))
.sort((a, b) => a.sortKey.localeCompare(b.sortKey)) // 使用 localeCompare 进行字符串比较
.map(item => item.originalElement); // 还原原始对象
console.log(sortedResult);关键点解析
- item.reference_id ?? '': 使用空字符串来代替 null 的 reference_id,确保拼接时不会出现 null1 这样的字符串。
- localeCompare(b.sortKey): 这是一个字符串比较方法,它会根据当前语言环境的排序规则进行比较,对于数字字符串也能较好地处理。
- 优点: 逻辑清晰,分步执行,易于理解。
- 缺点: 增加了额外的内存开销,因为需要创建中间对象数组。对于非常大的数据集,这可能成为一个考虑因素。
解决方案二:单次排序与自定义比较函数
为了避免创建中间对象,我们可以直接在 sort 方法的比较函数中动态生成排序键。这种方法更高效,但比较函数的逻辑可能略显复杂。
实现步骤
- 定义排序键生成函数: 创建一个辅助函数,它接收一个元素,并返回一个用于比较的字符串。
- 执行排序: 直接使用 Array.prototype.sort() 方法,并在比较函数中调用上述辅助函数来获取排序键。
示例代码
const arr = [
{ id: 1, name: 'hello world', reference_id: null },
{ id: 2, name: 'hello world', reference_id: null },
{ id: 3, name: 'hello world', reference_id: 1 },
{ id: 4, name: 'hello world', reference_id: null },
{ id: 5, name: 'hello world', reference_id: 1 },
{ id: 6, name: 'hello world', reference_id: 2 },
];
// 定义一个函数来生成元素的排序标准字符串
const getSortCriterion = (item) => {
// 1. 处理 reference_id:如果为 null,则视为空字符串。
// 2. 使用 padStart(4, "0") 确保数字字符串长度一致,防止 "10" < "2" 的错误比较。
// 例如:id=1 -> "0001", id=10 -> "0010"
// 3. 使用 "|" 作为分隔符,确保 reference_id 和 id 的组合不会混淆。
// 例如:ref=1, id=2 -> "0001|0002"
// ref=12, id=3 -> "0012|0003"
const refIdString = ("" + (item.reference_id ?? "")).padStart(4, "0");
const idString = ("" + item.id).padStart(4, "0");
return `${refIdString}|${idString}`;
};
// 对数组进行排序
const sortedResult = [...arr].sort((a, b) =>
getSortCriterion(a).localeCompare(getSortCriterion(b))
);
console.log(sortedResult);
console.log("计算出的排序标准(用于调试):");
console.log(sortedResult.map(getSortCriterion));关键点解析
- getSortCriterion(item) 函数:这是核心。
- item.reference_id ?? "": 同上,处理 null 值。
- .padStart(4, "0"): 这是解决数字字符串排序问题的关键。 如果不进行填充,"10" 会在 localeCompare 中被认为小于 "2"(因为它们都是字符串,按字符逐个比较)。通过 padStart,1 变为 "0001",10 变为 "0010",2 变为 "0002",这样 "0001"
- "|" 分隔符:确保 reference_id 和 id 的组合不会产生歧义。例如,如果 reference_id 是 1,id 是 23,组合是 123;如果 reference_id 是 12,id 是 3,组合也是 123。使用分隔符可以避免这种情况。
- [...arr].sort(...): sort() 方法会修改原数组。为了保持数组的不可变性(在React等框架中非常重要),我们通常会先创建一个浅拷贝 [...arr],然后再对其进行排序。
- 优点: 避免了创建中间数组,内存效率更高。比较函数可以处理更复杂的数字ID。
- 缺点: 比较函数内部逻辑相对复杂,需要理解 padStart 的作用。
注意事项与最佳实践
- 不可变性: Array.prototype.sort() 方法会直接修改原数组。在 React 或其他需要保持数据不可变的场景中,务必先创建数组的浅拷贝,例如 [...originalArray].sort(...),以避免副作用。
- ID 类型: 确保 id 和 reference_id 的数据类型一致,通常是数字或字符串。如果它们是混合类型,可能需要在比较函数中进行类型转换。
- padStart 的长度: 在解决方案二中,padStart(4, "0") 假设 id 最大为四位数。如果 id 可能更大(例如五位数或更多),则需要相应地增加填充长度(例如 padStart(5, "0"))。
- 性能考量: 对于非常大的数组,sort 方法的性能是 O(N log N)。在比较函数中执行复杂的字符串操作(如 padStart 和字符串拼接)会增加每次比较的开销。对于大多数前端应用,这通常不是问题,但如果遇到性能瓶颈,可能需要考虑其他数据结构或更优化的算法。
- 多层级排序: 这两种方法都适用于简单的父子层级。如果需要处理多层嵌套的层级结构(例如祖父-父-子),可能需要更复杂的递归排序逻辑或将数据转换为树形结构后再进行扁平化处理。
总结
通过构建自定义的排序键,我们可以有效地解决根据 id 和 reference_id 对数组进行复杂重排序的问题。无论是采用两步映射和排序的清晰方法,还是单次排序与自定义比较函数的更高效方法,关键都在于设计一个能够准确反映期望顺序的比较逻辑。在实际开发中,根据项目需求和数据规模选择最合适的方案,并始终注意保持代码的不可变性和可维护性。










