真实面经题目 · 原创解析

Array.sort的实现原理?

Array.prototype.sort 的核心不是某一个固定算法,而是 ECMAScript 规定排序语义,各 JavaScript 引擎自行选择实现。面试回答要区分三层:默认比较规则是转字符串后按 UTF-16 码元排序;传入 compareFn 后按返回值正负决定顺序;ES2019 之后规范要求稳定排序,但时间、空间复杂度仍由实现决定。以 V8 为代表的现代实现采用稳定排序方案,并需要处理 undefined、空槽、访问器、原型链、副作用等 JavaScript 动态特性。

出现于:阿里巴巴 · 前端

60 秒回答模板

可以这样回答:Array.sort 是原地排序方法,会修改原数组并返回同一个数组引用。规范主要定义排序结果应满足的比较语义,而不强制某个具体算法。没有传比较函数时,非 undefined 元素会先转成字符串,再按 UTF-16 码元的字典序比较,所以 [1, 30, 4] 默认不是数值升序。传入 compareFn 时,返回负数表示 a 在 b 前,返回正数表示 a 在 b 后,返回 0 或 NaN 表示两者相等并保持稳定排序关系。ES2019 起 sort 要求稳定,也就是比较结果相等的元素保持原有相对顺序;但不同引擎仍可选择不同算法。以 V8 为例,现代实现使用稳定的 TimSort,擅长利用已有有序片段,平均和最坏比较次数通常是 O(n log n),并在小片段上配合插入排序思想。还要补充:JavaScript 的 sort 比普通算法题更复杂,因为比较过程可能调用用户代码,比如 compareFn、toString、getter,这些都可能产生副作用,所以实现不仅要考虑算法,还要遵守规范中的读取、比较、写回、删除空槽等对象语义。

考点 规范层语义
主线 默认比较规则
易错点 把 Array.sort 简单回答成底层就是快速排序,…

深入解析

01

规范层语义

Array.prototype.sort 首先是一个 ECMAScript 内建方法,而不是某个固定排序算法的名字。规范描述的是:把接收者转成对象,读取 length,对已有的整数索引属性参与排序,再把排序后的值写回对象。它要求排序结果与比较函数语义一致,并且现在要求稳定排序,但并不指定必须使用快排、归并、堆排或 TimSort。因此不能只说底层是快排,更准确的说法是规范给语义,引擎选算法。

02

默认比较规则

如果没有传 compareFn,sort 不会按数字大小排序,而是把非 undefined 元素转换成字符串,再按 UTF-16 码元序比较。这个规则会导致 100 排在 21 前面,80 排在 9 前面,因为比较的是字符串中的字符序列而不是数值大小。undefined 会被移动到数组末尾,且不会传给比较函数;稀疏数组里的空槽也会被保留并移动到更靠后的位置。

03

比较函数契约

传入 compareFn 后,引擎会多次调用它来决定两个元素的相对顺序。返回小于 0,表示第一个参数排在第二个参数前;返回大于 0,表示第一个参数排在第二个参数后;返回 0 或 NaN,则认为两者排序键相等。一个可靠比较函数应满足纯函数、稳定一致、反对称、传递性等条件。若比较函数依赖随机数、修改数组、对同一对元素前后返回不同结果,最终顺序就可能不可预测。

04

稳定排序要求

稳定排序的意思是:如果两个元素在比较函数下相等,它们在结果中的相对先后顺序要与排序前一致。这个能力对多字段排序很重要,例如先按姓名排好,再按分数排序,分数相同的人仍能保持原先姓名顺序。ES2019 之前规范没有强制稳定,不同引擎、不同数组长度可能表现不一致;现在规范要求 Array.sort 稳定,但具体算法仍然由引擎自行决定。

05

引擎实现策略

现代引擎通常不会为 sort 选择单一朴素算法,而是根据数据规模、元素形态、已有有序程度和对象特性做工程化实现。V8 的稳定排序实现以 TimSort 为代表,结合归并排序和插入排序思想,能识别数组中天然存在的有序 run,并在近乎有序的数据上表现很好。这里要区分实现资料和语言规范:规范不强制所有引擎采用同一种算法。

06

JS 特有复杂性

JavaScript 的排序比 C 或 Java 中纯数组排序更复杂,因为元素访问本身可能执行用户代码。默认比较会调用元素的 toString,自定义比较会调用 compareFn,数组索引可能是 getter/setter,原型链上也可能存在索引属性。比较函数甚至可以在排序过程中 push、delete 或修改元素。引擎实现必须在性能优化和规范对象语义之间取平衡,所以不能简单套用算法书中的数组排序模型。

07

实际使用边界

业务中使用 sort 时要明确它会原地修改原数组,如果需要不可变数据,应先复制数组或使用 toSorted。数字排序要显式传入比较函数,例如数字升序通常用 (a, b) => a - b;字符串本地化排序可考虑 localeCompare,但它的成本更高。对象数组排序时应让比较函数只依赖稳定字段,不要在比较函数里改状态、发请求或产生副作用,否则结果和性能都难以维护。

易错点

  • 把 Array.sort 简单回答成底层就是快速排序,忽略规范并不绑定具体算法。
  • 忘记默认排序会转字符串,误以为数字数组不传比较函数也会按数值升序。
  • 认为 compareFn 必须返回 -1、0、1,其实引擎主要根据返回值的正负和相等关系判断。
  • 没有提 ES2019 之后稳定排序已经成为规范要求,仍停留在旧浏览器答案。
  • 在比较函数里修改数组、读写外部状态或使用随机数,导致排序结果不可预测。
  • 忽略 sort 会原地修改原数组,在不可变状态管理中直接调用引发数据污染。

面试官追问

为什么 [10, 2, 1].sort() 的结果不是 [1, 2, 10]?

因为默认 sort 会把元素转成字符串再比较,而不是进行数值比较。字符串比较时会逐个比较 UTF-16 码元,'10' 会排在 '2' 前面,因为第一位 '1' 小于 '2'。如果要数值升序,必须传入比较函数,例如用返回 a - b 的方式表达数字大小关系。

Array.sort 是稳定排序吗?

现代 JavaScript 中是稳定的。ES2019 起,规范要求 Array.prototype.sort 保持稳定,也就是 compareFn 判定相等的元素不能随意改变原有相对顺序。但历史上并非如此,旧浏览器或旧运行时可能对短数组稳定、长数组不稳定,所以回答时最好说明当前规范要求稳定,旧实现不一定。

V8 的 Array.sort 底层到底是什么算法?

V8 的现代稳定排序实现以 TimSort 为代表。TimSort 会识别已有有序片段并进行归并,对接近有序的数据很友好,也会在小片段中使用类似插入排序的策略降低常数成本。但这只是 V8 的实现选择,不能把它当成所有 JavaScript 引擎或 ECMAScript 规范的统一答案。

compareFn 能不能写成随机返回值?

不应该。compareFn 需要对同一对元素给出一致结果,并满足基本的排序关系。如果使用 Math.random、修改外部状态,或让 a 小于 b、b 小于 c、但 a 又大于 c,排序算法就无法建立可靠顺序。不同引擎和不同执行次数可能得到不同结果,业务代码也会非常难排查。

sort 为什么会影响原数组?

Array.prototype.sort 是原地排序方法,它会把排序后的元素写回原数组,并返回这个数组本身的引用。因此在 React 状态、Redux 数据或其他不可变数据场景中,直接调用 sort 可能造成隐式 mutation。需要保留原数组时,可以先复制再排序,或在支持的环境中使用 toSorted。