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, ...
随机推荐
- 渐变边框文字效果?CSS 轻松拿捏!
今天,有个群友问了我这么一个问题,如果不想切图,是否有办法实现带渐变边框的字体效果?如下所示: 本文,就将尝试一下,在 CSS 中,我们可以如何尽可能的实现这种渐变边框字体效果. 元素叠加 首先,比较 ...
- 阅读翻译Mathematics for Machine Learning之2.7 Linear Mappings
阅读翻译Mathematics for Machine Learning之2.7 Linear Mappings 关于: 首次发表日期:2024-07-23 Mathematics for Machi ...
- Kmesh v0.4发布!迈向大规模 Sidecarless 服务网格
本文分享自华为云社区<Kmesh v0.4发布!迈向大规模 Sidecarless 服务网格>,作者: 云容器大未来. 近日 Kmesh 发布了 v0.4.0 版本,感谢社区的贡献者在两个 ...
- Scratch植物大战僵尸全套素材包免费下载
scratch植物大战僵尸全套素材包,包含227个丰富多样的素材,涵盖角色.背景.动态gif.为Scratch创作者提供丰富资源,助力创作精彩作品. 免费下载地址:www.xiaohujing.com ...
- LeetCode654. 最大二叉树
题目链接:https://leetcode.cn/problems/maximum-binary-tree/description/ 题目叙述 给定一个不重复的整数数组 nums . 最大二叉树 可以 ...
- docker centos8 java8 mysql8 部署springboot项目
docker centos8 java8 mysql8 部署springboot项目 一,用idea将springboot项目打成jar包 二,将打的jar包用xshell的rz上传到docker的c ...
- 【Vue】代理服务配置
Springboot 后台接口地址 基础路径配置: server: port: 8080 servlet: context-path: /demo 完整路径: localhost:8080/demo ...
- 【Spring】03 XML配置
Alias别名设置 可以为一个Bean的ID再设置一个ID 多一个可用标识,大概... 在获取实例注入参数时,两个标识都可以使用 除了Alias可以设置别名之外,Bean的标签本身也可以设置第二别名 ...
- 【Spring Data JPA】05 方法名限定查询
方法名限定查询 方法名限定查询是对JPQL的再封装 按照SpringData提供的方法名定义方法,不需要配置JPQL语句即可完成查询 在IDEA中都有相应的提示 他会按照方法字符判断 public C ...
- Electronics投稿指南
原地址: https://m.peipusci.com/news/10593.html Electronics的自引率先增后减,2023年度为10.3%.