前言

排序是很常见的需求. 虽然看似简单, 但其实暗藏杀机. 一不小心就会搞出 Bug 哦.

这篇就来聊聊 JS 的排序.

参考

原生JS数组sort()排序方法内部原理探究

值的比较

js中的localeCompare到底是如何比较的?

直觉和特殊场景

说到排序. 一般人熟悉的情况是这些

直观的

英文 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的更多相关文章

  1. javascript sort()与reverse()

    javascript 中提供了两个对数据进行排序的方法,即sort()和reverse() 在理解的时候犯了一个非常低级的错误,现记录如下: reverse()不包括排序的功能,只是把原来的数组反转. ...

  2. javascript sort 用法

    <html> <head> <title></title> <script type="text/javascript" sr ...

  3. JavaScript sort() 方法

    定义和用法 sort() 方法用于对数组的元素进行排序. 语法 arrayObject.sort(sortby) 参数 描述 sortby 可选.规定排序顺序.必须是函数. 返回值 对数组的引用.请注 ...

  4. javascript sort方法容易犯错的地方

    sort方法用来对数组排序非常方便.但是sort(func)这个func参数的构造却很容易混淆. sort判断func的返回值是判断正负,而不是ture和false.所以务必保证返回值要么负数要么正数 ...

  5. Javascript:sort()方法快速实现对数组排序

    定义和用法: sort() 方法用于对数组的元素进行排序. 语法: arrayObject.sort(sortby) 注释:sortby,可选,规定排序顺序,必须是函数. 说明: 如果调用该方法时没有 ...

  6. javascript sort排序

    var arr = [5,32,28,66,2,15,3]; arr.sort(function(a1,a2){ return a1-a2; //a2-a1 输入倒序 }); console.log( ...

  7. JavaScript sort()方法比较器

    当我们想把一个由数字组成的数组进行简单的排序时,可能会想到sort()方法: var arr = [2 , 3, -1, -107, -14, 1]; console.log(arr.sort()) ...

  8. JavaScript sort() 方法详解

    定义和用法 sort() 方法用于对数组的元素进行排序. 语法 arrayObject.sort(sortby) 参数 描述 sortby 可选.规定排序顺序.必须是函数. 返回值 对数组的引用.请注 ...

  9. Javascript sort方法

    sort()方法用于对数组的元素进行排序 语法:array.Object.sort(sortBy) sortBy:可选.规定排序顺序.必须是函数 返回值:对数组的引用.数组在原数组上进行排序,不生成副 ...

  10. javascript sort 函数用法

    sort 函数 博客地址:https://ainyi.com/41 简单的说,sort() 在没有参数时,返回的结果是按升序来排列的.即字符串的Unicode码位点(code point)排序 [5, ...

随机推荐

  1. 渐变边框文字效果?CSS 轻松拿捏!

    今天,有个群友问了我这么一个问题,如果不想切图,是否有办法实现带渐变边框的字体效果?如下所示: 本文,就将尝试一下,在 CSS 中,我们可以如何尽可能的实现这种渐变边框字体效果. 元素叠加 首先,比较 ...

  2. 阅读翻译Mathematics for Machine Learning之2.7 Linear Mappings

    阅读翻译Mathematics for Machine Learning之2.7 Linear Mappings 关于: 首次发表日期:2024-07-23 Mathematics for Machi ...

  3. Kmesh v0.4发布!迈向大规模 Sidecarless 服务网格

    本文分享自华为云社区<Kmesh v0.4发布!迈向大规模 Sidecarless 服务网格>,作者: 云容器大未来. 近日 Kmesh 发布了 v0.4.0 版本,感谢社区的贡献者在两个 ...

  4. Scratch植物大战僵尸全套素材包免费下载

    scratch植物大战僵尸全套素材包,包含227个丰富多样的素材,涵盖角色.背景.动态gif.为Scratch创作者提供丰富资源,助力创作精彩作品. 免费下载地址:www.xiaohujing.com ...

  5. LeetCode654. 最大二叉树

    题目链接:https://leetcode.cn/problems/maximum-binary-tree/description/ 题目叙述 给定一个不重复的整数数组 nums . 最大二叉树 可以 ...

  6. docker centos8 java8 mysql8 部署springboot项目

    docker centos8 java8 mysql8 部署springboot项目 一,用idea将springboot项目打成jar包 二,将打的jar包用xshell的rz上传到docker的c ...

  7. 【Vue】代理服务配置

    Springboot 后台接口地址 基础路径配置: server: port: 8080 servlet: context-path: /demo 完整路径: localhost:8080/demo ...

  8. 【Spring】03 XML配置

    Alias别名设置 可以为一个Bean的ID再设置一个ID 多一个可用标识,大概... 在获取实例注入参数时,两个标识都可以使用 除了Alias可以设置别名之外,Bean的标签本身也可以设置第二别名 ...

  9. 【Spring Data JPA】05 方法名限定查询

    方法名限定查询 方法名限定查询是对JPQL的再封装 按照SpringData提供的方法名定义方法,不需要配置JPQL语句即可完成查询 在IDEA中都有相应的提示 他会按照方法字符判断 public C ...

  10. Electronics投稿指南

    原地址: https://m.peipusci.com/news/10593.html Electronics的自引率先增后减,2023年度为10.3%.