JavaScript – Sort
前言
排序是很常见的需求. 虽然看似简单, 但其实暗藏杀机. 一不小心就会搞出 Bug 哦.
这篇就来聊聊 JS 的排序.
参考
直觉和特殊场景
说到排序. 一般人熟悉的情况是这些
直观的
英文 a 到 z 顺序
中文 阿, 八, 差, 依据汉语拼音的英文字母顺序
数字 -1 < 0 < 1 negative < zero < positive 小到大
日期 01-01-2023, 02-01-2023, 03-01-2023 过去到未来
这些都很直观, 但是真实情况却往往会有超出我们的预料, 比如
特殊的
a 和 A 字母大小写区别. 先 a 还是先 A?
null or undefined or empty string or NaN 排前面还是后面?
不同类型对比是怎样, 100 > 'abc'?
符号对比, '~' > '@' 谁先谁后?
老实说这种奇葩场景就不应该存在. 因为它们就是来乱的丫.
String Comparison
两个字符串比大小 (e.g. 'abc', 'xyz')
首先各自取出第一个字符 (e.g. 'a' vs 'x')
然后转换成 Unicode
'a'.charCodeAt(0); // 97
'x'.charCodeAt(0); // 120
然后比大小. 97 小于 120, 所以 "顺序" 的情况下, 字符 'a' 在 字符 'z' 的前面
结论: 对于字符串, 它是一个一个逐个转换成 Unicode 比较得出结果的 (如果第一个字符相同, 那就继续比第二个. 直到不相同来分胜负)
另外, empty string 的 Unicode 是 0 所以 empty string 总是在前面.
['a', ''].sort(); // ['', 'a'] empty Unicode 0 所以前面
['abc', 'abcd'].sort(); // 前面 3 个字符相同, 第 4 个是 emtpty string vs 'd' 也就是 0 vs 100 所以 abc 胜
以上逻辑也适用于 C#
Number Comparison
-1 < 0 < 1
negative < zero < positive
这个很好理解. 但是 JS 中有一个奇葩叫 NaN
它是一个数字又不是一个数字. 它的特色是无论和什么数字比大小结果都是 false
NaN > 0; // false
NaN < 0; // false
NaN > 1; // false
NaN < 1; // false
它不比任何数字大, 也不比任何数字小 ....
JS comparison auto convert type
a > b 当 a,b 类型不相同时, JS 会把它们都转换成 Number 来对比. 以前在 JavaScript – 类型转换 也有提到过.
当然还是建议不要让它自动转换的好.
Array.sort 默认行为
好, 我们已经有一点基础了. 来看看 JS 的 Array.sort 是如何排序的吧.
我们不注重它使用了什么排序方式 (插排, 快排, 冒泡排). 我们只关心它排序的结果.
['z', 'b', 'a'].sort(); // ["a", "b", "z"]
['差' , '八', '阿'].sort(); // ["八", "差", "阿"]
[1, 11, 2, 3].sort(); // [1, 11, 2, 3]
[null, undefined, 'm', 'o', 't', 'v'].sort(); // ["m", null, "o", "t", "v", undefined]
[new Date('2023-01-01'), new Date('2023-01-02'), new Date('2023-01-04')].sort(); // [2号, 1号, 4号]
第一个正常
第二个...汉字并没有依据汉语拼音排序
第三个... 11 比 2,3 小?
第四个... null 在 m 和 o 中间? undefined 在最后?
第五个... 2号比1号早 ?
真的是奇葩到...不能用丫.
Array.sort 默认的行为是这样的. 首先把所有的值强转成 string, 然后进行 string comparison.
第二题的中文字, 因为 string comparison 是比 Unicode 的号码, 而不是依据汉语拼音, 所以顺序就不对了.
['差' , '八', '阿'].map(v => v.charCodeAt(0)); // [24046, 20843, 38463]
转成 Unicode 比较后顺序是 20843 差, 24046 八, 38463 阿.
第三题 11 被转换成了 string '11', string comparison 是逐个字母对比的, 于是
'11' vs '2' = '1' vs '2' = Unicode 49 vs 50. 结果 '11' 获胜
第四题 null 被强转 string 的结果是 'null' 而 undefined 强转 string 的结果 'undefined'
于是 string comparison 结果 'm', 'null', 'o' 就可以理解了. 但是 undefined 理应在 't', 'undefined', 'v' 丫. 但却在最后.
这是因为它是一个特殊对待 Stack Overflow – javascript array.sort with undefined values, undefined 总是排在最后面.
第五题 日期被强转成 string 后变成
1号 = Sun Jan 01 2023 08:00:00 GMT+0800 (Malaysia Time)
2号 = Mon..
4号 = Wed...
于是 string comparison 的顺序是 Mon, Sun, Wed = 2号, 1号, 4号
自定义 Array.sort
Array.sort 默认行为很难用于真实的开发场景. 所以我们需要自定义. 它允许我们提供一个 comparison 方法.
[1, 11, 2, 3].sort((a, b) => a - b); // [1, 2, 3, 11]
这样就正常了.
它的工作原理是这样的
a 和 b 是 2 个 array 中的值, 我们不需要理会这 2 个的出现顺序和次数. 这些会依据 JS 使用的排序方法而定.
我们只关心这 2 个值对比后哪一个胜出就可以了
当获取到 a = 2, b = 11 时.
如果我们想表达 a 小于 b 那么方法就返回 negative, 想表达 a 大于 b 就返回 positive, 想表达 2 个值相等就返回 0
接着 JS 就会处理后续的事儿了.
所以看回上面的代码 return a - b
当 a = 2, b = 11
a - b = -9 negative 表示 a 小于 b
至此我们就可以完全掌控要如何排序了
1. 数字排序
[1, 11, 2].sort(); // [1, 11, 2]
[1, 11, 2].sort((a, b) => a - b); // [1, 2, 11]
2. 大小写排序
console.log(['a', 'A', 'b', 'B'].sort()); // ["A", "B", "a", "b"]
console.log(['a', 'A', 'b', 'B'].sort((a, b) => a.localeCompare(b))); // ["a", "A", "b", "B"]
sort 默认是依据 Unicode 排序, 那么大写字母肯定都在小写字母前面. 如果不希望这样的话, 可以改用 localeCompare
这个 localeCompare 的排序方式比较人性化. 它是小写在前面, 而且 A 也小于 b. C# LINQ 应该也是这样的. SQL Server 则默认是不区分大小写的
3. 中文排序
['差' , '八', '阿'].sort(); // ["八", "差", "阿"]
['差' , '八', '阿'].sort((a, b) => a.localeCompare(b, 'zh-CN')); // ["阿", "八", "差"]
同样是用了 localeCompare 方法, 通过 'zh-CN' 表达是简体汉字, 于是它就变成了用汉语拼音排序. 确实挺人性化的.
这个 locale 的标准是 BCP 47, 所以通常是用 en-US 和 zh-CN (简体中文) / zh-TW (繁体中文)
题外话, 要判断字符串是不是汉字其实挺难的.
参考:
Stack Overflow – What's the complete range for Chinese characters in Unicode?
Stack Overflow – Is there a way to check whether unicode text is in a certain language? (C#)
思路大概是这样 (注: 如果你对 Unicode, 十进制, 十六进制不熟悉, 先看这篇 Bit, Byte, ASCII, Unicode, UTF, Base64)
首先我们要知道汉字的 Unicode range. 这个超级多的, 但凡有 CJK 开头的都可以算是汉字, 但是汉字不只是中国的, 日本韩国也参进去了. 有些汉字只有日本有, 这些都比较乱水.
我拿最 common 的来举例

4E00 – 9FFF 是其中一个 range. 它使用十六进制来表达.
先把它换成十进制, 这样比较容易处理
const chineseRangeFrom = '4E00';
const chineseRangeTo = '9FFF'; // 把 十六进制 转成 十进制
const from = parseInt(chineseRangeFrom, 16); // 19968
const to = parseInt(chineseRangeTo, 16); // 40959
假设我们有一个汉字 "严"
const value = '严';
// convert to Unicode (十进制)
const unicode = value.charCodeAt(0); // 20005 这个是十进制
我们通过 charCodeAt 找出它的 Unicode 号. 这个返回的是十进制哦.
最后我们可以判断这个号是否在 range 里面. 在 range 里面的就表示它是汉字.
if(unicode >= from && unicode <= to) {
console.log('yes is chinese char');
}
题外话, C# 是通过 set global culture 来实现的...非常友好!
Thread.CurrentThread.CurrentCulture = new System.Globalization.CultureInfo("zh-Hans");
var values = new string[] { "差", "阿", "八" };
var newValues = values.Order(); // ["阿", "八", "差"]
5. null 的排序
SQL Server 和 C# LINQ order by 都是把 null 放前面 (顺序), 但不是所以 Database 都这样哦. JS 则完全我们自己决定.
console.log(['m', 'o', null].sort()); // ["m", null, "o"]
console.log(['m', 'o', null].sort((a, b) => {
if(a === null && b === null) return 0
if(a === null) return -1; // 想 null 在后面这里就返回 1
if(b === null) return 1; // 想 null 在后面这里就返回 -1
return a.localeCompare(b);
})); // [null, "m", "o"]
5. 日期排序
console.log([
new Date('2023-01-01'),
new Date('2023-01-02'),
new Date('2023-01-04')
].sort()); // 2号, 1号, 4号 console.log([
new Date('2023-01-01'),
new Date('2023-01-02'),
new Date('2023-01-04')
].sort((a, b) => a.getTime() - b.getTime())); // 1号, 2号, 4号
这里需要注意一点, 如果出现 invalid date 该怎么处理呢? 放任它不管的话, invalid date getTime 会得到 NaN
而 sort 函数理应返回 negative, zero, positive. 当返回 NaN 时, 它的效果相等于返回 zero.
切记排序时要想清楚所有可能出现的 value, 并且明确表面它们的顺序.
还有一个无敌 modern 的方式是用 Temporal API (目前 14-01-2023 还没有游览器支持)
6. 不同类型的排序
避开!!! 或者自己强转类型到一致. 不要出现任何 'a' > 123 这种鬼东西.
7. 字母, 数排序
字母中如果出现数字怎么排序呢?
const values = ['1', 'b', '11', '3', 'a', '2'];
values.sort((a,b) => a.localeCompare(b, 'en-US')); // ["1", "11", "2", "3", "a", "b"]
values.sort((a,b) => a.localeCompare(b, 'en-US', { numeric: true })); // ["1", "2", "3", "11", "a", "b"]
需要加入 numeric. 但我上面说过了, 不要 order 2 个不同类型. 你看 SQL, C# order by 的结果都不会考虑 numeric 的.
如果对 C# 怎么实现 numeric 感兴趣, 可以看这篇: Stack Overflow – How do I sort strings alphabetically while accounting for value when a string is numeric? 里面有许多 hacking way 非常聪明.
Intl.Collator vs localeCompare
参考: 张鑫旭 – JS Intl对象完整简介及在中文中的应用
Intl.Collator 据说是比 localeCompoare 更 modern 一点.
两个的接口都差不多.
const chineses = ['差', '阿', '八'];
chineses.sort(new Intl.Collator('zh-CN').compare); // ["阿", "八", "差"]
逆序
顺序之后利用 Array.reverse 实现逆序是很不错的招数.
不然就在自定义的时候返回相反逻辑. 比如顺序返回 -1 negative 的话, 想逆序就返回 1 positive.
Multiple Order By
这个只会出现在 order by object 上. 它的做法就是当第一个 property value 相同时, 不要返回 0.
而是继续 compare 第二个 property value.
总结
JS 的 Array.sort 原生几乎是不可以使用的. 它的逻辑是先强转所以 value 去 string 然后依据 Unicode 排序.
几乎只有 a-z 可以符合这个做法. 连 number array 都 sort 不正确了. 更不用提 null, undefined 这些鬼.
自定义 sort 可以完成所有需求. 但一定要留意所有 value 的可能性. JS 在 compare value 是会有许多奇葩的自动转换规则.
我们要尽量避开让它自动转换, 自己强转并且明确表明哪一个值比较小或大. 这样排序结果才能正确.
JavaScript – Sort的更多相关文章
- javascript sort()与reverse()
javascript 中提供了两个对数据进行排序的方法,即sort()和reverse() 在理解的时候犯了一个非常低级的错误,现记录如下: reverse()不包括排序的功能,只是把原来的数组反转. ...
- javascript sort 用法
<html> <head> <title></title> <script type="text/javascript" sr ...
- JavaScript sort() 方法
定义和用法 sort() 方法用于对数组的元素进行排序. 语法 arrayObject.sort(sortby) 参数 描述 sortby 可选.规定排序顺序.必须是函数. 返回值 对数组的引用.请注 ...
- javascript sort方法容易犯错的地方
sort方法用来对数组排序非常方便.但是sort(func)这个func参数的构造却很容易混淆. sort判断func的返回值是判断正负,而不是ture和false.所以务必保证返回值要么负数要么正数 ...
- Javascript:sort()方法快速实现对数组排序
定义和用法: sort() 方法用于对数组的元素进行排序. 语法: arrayObject.sort(sortby) 注释:sortby,可选,规定排序顺序,必须是函数. 说明: 如果调用该方法时没有 ...
- javascript sort排序
var arr = [5,32,28,66,2,15,3]; arr.sort(function(a1,a2){ return a1-a2; //a2-a1 输入倒序 }); console.log( ...
- JavaScript sort()方法比较器
当我们想把一个由数字组成的数组进行简单的排序时,可能会想到sort()方法: var arr = [2 , 3, -1, -107, -14, 1]; console.log(arr.sort()) ...
- JavaScript sort() 方法详解
定义和用法 sort() 方法用于对数组的元素进行排序. 语法 arrayObject.sort(sortby) 参数 描述 sortby 可选.规定排序顺序.必须是函数. 返回值 对数组的引用.请注 ...
- Javascript sort方法
sort()方法用于对数组的元素进行排序 语法:array.Object.sort(sortBy) sortBy:可选.规定排序顺序.必须是函数 返回值:对数组的引用.数组在原数组上进行排序,不生成副 ...
- javascript sort 函数用法
sort 函数 博客地址:https://ainyi.com/41 简单的说,sort() 在没有参数时,返回的结果是按升序来排列的.即字符串的Unicode码位点(code point)排序 [5, ...
随机推荐
- 解决方案 | cvxpy成功安装过程及其使用攻略
背景: 由于需要研究KKT条件下的最优化问题,需要安装一个python的包cvxpy. 过程: 1.正常pip install cvxpy 不可取(不会成功,中间有报错): 2.主要错误在于:其依赖 ...
- 10.2 web服务器
Web客户端和服务器之间的交互用的是一个基于文本的应用级协议,叫做HTTP(Hypertext Transfer Protocol,超文本传输协议).HTTP是一个简单的协议.一个Web客户端(即浏览 ...
- [oeasy]python0073_进制转化_eval_evaluate_衡量_oct_octal_八进制
进制转化 回忆上次内容 上次了解的是 整型数字类变量 integer 前缀为i 添加图片注释,不超过 140 字(可选) 整型变量 和 字符串变量 不同 整型变量 是 直接存储二进制形 ...
- SSH指定用户登录与限制
环境准备 :::info 实验目标:ServerA通过用户ServerB(已发送密钥和指定端口) ::: 主机 IP 身份 ServerA 192.168.10.201 SSH客户端 ServerB ...
- OpenGL 4.0中数据缓冲VBO,VAO,EBO的使用总结
Opengl是大家常用的一个API,我们用它绘制数据的时候需要使用vao,vbo,ebo等对象,绘制方式分为 vao绘制,ebo绘制等.使用不同api还能分为普通调用以及Instance绘制. 首先申 ...
- 关于mybatisplus与mybatis的自动填充混用问题
public class MybatisPlusAutoFillHandler implements MetaObjectHandler { //插入时的填充策略 @Override public v ...
- 基于Drone实现CI/CD【0到1架构系列】
CI/CD是持续性集交和持续性部署,简单来讲就是自动化构建和自动化部署.目前有很多集成方案,也有很多组装方案,只要能实现自动化构建出制品,再自动部署到生产环境就行. 目前很多源代码都集成了CI/CD功 ...
- git篇-- Git在项目实操中常见的使用命令--02
Git是现代软件开发中不可或缺的版本控制工具.它能帮助开发者跟踪项目的所有变更,并与团队成员高效协作.本文将介绍一些在项目实操中常见的Git命令,帮助你更好地管理代码. 1. 初始化和配置 初始化仓库 ...
- 【Spring】06 Aop切面功能
什么是Aop? Aspect Oriented Programming 面向切面编程 通过预编译的方式和运行期动态代理实现程序功能统一维护的一种技术 是OOP的延续,也是Spring第二个核心内容 可 ...
- 【Centos】RPM安装Mysql8
先去官网下载RPM包,没想到RPM包是红帽发行版 https://dev.mysql.com/downloads/mysql/ 使用wget直接下载到Centos里面: wget https://cd ...