如何在JavaScript中对字符串进行排序

526

我有一个对象列表,希望按照类型为字符串的字段attr进行排序。我尝试使用-

list.sort(function (a, b) {
    return a.attr - b.attr
})

但是我发现在 JavaScript 中,- 似乎不能用于字符串。那么如何根据一个字符串类型的属性对对象列表进行排序呢?

但我发现JavaScript中-不能用于字符串。如何基于字符串类型的属性对对象列表进行排序?


1
请参考 https://dev59.com/IXI95IYBdhLWcg3wzhd9 中的 JavaScript 不区分大小写字符串比较。 - Adriano
对于快速的“国际化”解决方案(我猜只是部分,因为这可能无法涵盖世界上所有口音),您可能希望简单地忽略口音,也就是去掉它们。然后只进行字符串比较,请参见https://dev59.com/OnNA5IYBdhLWcg3wX8nk上的`Javascript:remove accents / diacritics in strings`。 - Adriano
2
有趣的是,Jeff Atwood本人在2007年写了一篇关于这个常见问题的博客文章,请参见http://blog.codinghorror.com/sorting-for-humans-natural-sort-order/。 - Adriano
这是一个非常古老的问题,所以如果你像我一样从未来看到这个问题,你应该在实施任何你在这里找到的建议之前,真正阅读关于性能的这个问题 - Coderer
16个回答

910

143
在任何人犯我所犯过的匆忙错误之前,请注意,这是localeCompare,而不是localCompare。 - ento
20
第一个解决方案将认为字母"A"排在字母"Z"之后但在"z"之前,因为它是按照字符ASCII值进行比较的。localeCompare()不会遇到这个问题,但是它不能识别数字,所以你得到的结果将是["1", "10", "2"],这与大多数语言中的排序比较相同。如果你想要在UI前端进行排序,请查看alphanum/natural排序算法https://dev59.com/22855IYBdhLWcg3wcz1h或https://dev59.com/8FLTa4cB1Zd3GeqPZE32。 - Dead.Rabit
2
请注意,localeCompare()仅受现代浏览器支持:在撰写本文时,仅支持IE11+,请参见https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/localeCompare。 - Adriano
4
不,我指的是表格的第一行,@Adrien - IE在很多版本中支持 localeCompare(),但直到11版才支持*指定区域设置(locale)*。还要注意Dead.Rabit链接的那些问题。 - Shog9
3
@Shog9 不好意思,看起来从IE6开始就支持了!请参见(滚动/搜索到localeCompare()方法)http://msdn.microsoft.com/en-us/library/ie/s4esdbwz(v=vs.94).aspx。需要注意的一点是,在旧的实现中,如果我们不使用locales和options参数(即在IE11之前使用的方式),则使用的区域设置和排序顺序完全取决于实现,换句话说:Firefox、Safari、Chrome和IE不会按相同顺序对字符串进行排序。请参见https://code.google.com/p/v8/issues/detail?id=459 - Adriano
显示剩余6条评论

219

一个更新的答案(2014年10月)

我对这个字符串的自然排序顺序感到非常恼火,所以花了很多时间来调查这个问题。

长话短说

localeCompare()的字符支持非常强大,只需使用它即可。 正如Shog9所指出的,对于你的问题的答案是:

return item1.attr.localeCompare(item2.attr);

所有自定义JavaScript“自然字符串排序顺序”实现中发现的错误

有很多自定义实现尝试更精确地进行字符串比较,称为“自然字符串排序顺序”。

当使用这些实现时,我总是注意到一些奇怪的“自然排序顺序”选择,或者说是错误(或者在最好的情况下是遗漏)。

通常,特殊字符(空格、破折号、和号、括号等)没有被正确处理。

你会发现它们混在不同的位置出现,通常可能是:

  • 一些会出现在大写字母'Z'和小写字母'a'之间
  • 一些会出现在数字'9'和大写字母'A'之间
  • 一些会出现在小写字母'z'之后

当人们期望特殊字符都被“分组”在一起的时候,除了空格特殊字符(它通常是第一个字符)。也就是说,要么全部在数字之前,要么全部在数字和字母之间(小写字母和大写字母“连在一起”),要么全部在字母之后。

我的结论是,当我开始添加一些不太常见的字符(例如带有变音符号的字符或破折号、感叹号等字符)时,它们都无法提供一致的顺序。
对自定义实现的研究:

浏览器原生的“自然字符串排序顺序”实现通过localeCompare()

localeCompare()最早的实现(不包括locales和options参数)支持Internet Explorer 6及更高版本,请参见Legacy Microsoft Edge开发者文档(向下滚动至localeCompare()方法)。

内置的localeCompare()方法在排序方面做得更好,甚至可以处理国际和特殊字符。

使用localeCompare()方法的唯一问题是{{link4:“所使用的区域设置和排序顺序完全取决于实现”。换句话说,当使用localeCompare比如stringOne.localeCompare(stringTwo)时:Firefox、Safari、Chrome和Internet Explorer对字符串有不同的排序顺序。}}

关于浏览器原生实现的研究:

“字符串自然排序顺序”的难点

实现一个稳定的算法(即:一致性且涵盖广泛的字符范围)是一项非常艰巨的任务。UTF-8包含超过2000个字符,并且涵盖了120多种文字(语言)

最后,对于这个任务有一些规范,它被称为“Unicode排序算法”。您可以在我发布的这个问题上找到更多信息,是否有一种与语言无关的“字符串自然排序顺序”规范?

最终结论

考虑到我所遇到的JavaScript自定义实现提供的当前支持水平,我们可能永远不会看到任何接近支持所有这些字符和脚本(语言)的东西。因此,我宁愿使用浏览器的本地localeCompare()方法。是的,它的缺点是在不同浏览器之间不一致,但基本测试显示它涵盖了更广泛的字符范围,允许稳定和有意义的排序顺序。
所以正如Shog9所指出的那样,对你的问题的答案是:
return item1.attr.localeCompare(item2.attr);

进一步阅读:

感谢Shog9的精彩回答,它让我相信我走在了“正确”的方向上。

“scroll down to”附近的链接(实际上)已经失效。它会重定向到一个不明确的页面(该页面上没有localeCompare())。 - undefined

74

现代ECMAScript中的答案

list.sort((a, b) => (a.attr > b.attr) - (a.attr < b.attr))
或者
list.sort((a, b) => +(a.attr > b.attr) || -(a.attr < b.attr))

描述

将布尔值转换为数字的结果如下:

  • true -> 1
  • false -> 0

考虑三种可能的模式:

  • x 大于 y:(x > y) - (y < x) -> 1 - 0 -> 1
  • x 等于 y:(x > y) - (y < x) -> 0 - 0 -> 0
  • x 小于 y:(x > y) - (y < x) -> 0 - 1 -> -1

(替代方案)

  • x 大于 y:+(x > y) || -(x < y) -> 1 || 0 -> 1
  • x 等于 y:+(x > y) || -(x < y) -> 0 || 0 -> 0
  • x 小于 y:+(x > y) || -(x < y) -> 0 || -1 -> -1

因此,这些逻辑与典型的排序比较函数等效。

if (x == y) {
    return 0;
}
return x > y ? 1 : -1;

2
正如我在评论早先的回答中所提到的,仅包含代码的答案可以通过解释其工作原理来使其更有用。 - Dan Dascalescu
添加了描述 - mpyw
4
localeCompare 和标准比较产生不同的结果。你希望哪一个?["A", "b", "C", "d"].sort((a, b) => a.localeCompare(b)) 按字母顺序(不区分大小写)排序,而 ["A", "b", "C", "d"].sort((a, b) => (a > b) - (a < b)) 按 Unicode 码点顺序排序。 - mpyw
我明白了,那似乎是一个主要的瓶颈。有关于性能差异的任何想法吗? - Ran Lottem
2
这比localeCompare更好的原因是,localeCompare会对不相等的字符串返回0。具体例子:有(至少)两个看起来相同的“Ö”符号,但localeCompare会认为它们相同,但它们在===时失败(即使大写)。因此,您漂亮的UI代码可能会执行涉及排序和分组的操作,并认为“Ö”与“Ö”相同,但使用Map的后端逻辑将决定这两个Ö是不同的,因此事情变得糟糕。 - Kevin Frei
显示剩余2条评论

28

由于在JavaScript中可以直接比较字符串,因此这样做就可以了:

list.sort(function (a, b) {
    return a.attr < b.attr ? -1: 1;
})

这比使用的方法稍微更有效率一些。

  return a.attr > b.attr ? 1: -1;
因为在具有相同属性的元素的情况下(a.attr == b.attr),排序函数会无故地交换两者位置。
例如:
  var so1 = function (a, b) { return a.atr > b.atr ? 1: -1; };
  var so2 = function (a, b) { return a.atr < b.atr ? -1: 1; }; // Better

  var m1 = [ { atr: 40, s: "FIRST" }, { atr: 100, s: "LAST" }, { atr: 40, s: "SECOND" } ].sort (so1);
  var m2 = [ { atr: 40, s: "FIRST" }, { atr: 100, s: "LAST" }, { atr: 40, s: "SECOND" } ].sort (so2);

  // m1 sorted but ...:  40 SECOND  40 FIRST   100 LAST
  // m2 more efficient:  40 FIRST   40 SECOND  100 LAST

1
这并不完全正确,比较函数在两个字符串相等时必须返回0。 - alextgordon
这个比较函数可以正确地对列表进行排序,但如果需要移动具有相同值的元素,则可以通过“return a.attr == b.attr ? 0: a.attr > b.attr ? 1: -1;”进行更改。 - Alejadro Xalabarder
实际上,比较 a.attr == b.attr 是不必要的,因为函数返回的结果为 0 会告诉排序算法什么都不做,与结果 -1 相同。 - Alejadro Xalabarder

15

您应该在此处使用>或<和==。因此,解决方案将是:

list.sort(function(item1, item2) {
    var val1 = item1.attr,
        val2 = item2.attr;
    if (val1 == val2) return 0;
    if (val1 > val2) return 1;
    if (val1 < val2) return -1;
});

1
顺便提一下,这个方法无法处理字符串与数字的比较。例如:'Z' < 9(false),'Z' > 9(也是false??),'Z' == 9(还是false!!)。JavaScript中的NaN真是傻傻的... - Kato

14

嵌套三元箭头函数

(a,b) => (a < b ? -1 : a > b ? 1 : 0)

9
一个使用自定义函数返回升序或降序排列的已排序字符串的 TypeScript 排序方法修饰符。
const data = ["jane", "mike", "salome", "ababus", "buisa", "dennis"];

const sortStringArray = (stringArray: string[], mode?: 'desc' | 'asc') => {
  if (!mode || mode === 'asc') {
    return stringArray.sort((a, b) => a.localeCompare(b))
  }
  return stringArray.sort((a, b) => b.localeCompare(a))
}

console.log(sortStringArray(data, 'desc'));// [ 'salome', 'mike', 'jane', 'dennis', 'buisa', 'ababus' ]
console.log(sortStringArray(data, 'asc')); // [ 'ababus', 'buisa', 'dennis', 'jane', 'mike', 'salome' ]



9
为什么问题中的方法行不通的解释:
let products = [
    { name: "laptop", price: 800 },
    { name: "phone", price:200},
    { name: "tv", price: 1200}
];
products.sort( (a, b) => {
    {let value= a.name - b.name; console.log(value); return value}
});

> 2 NaN

字符串之间的减法运算返回NaN。

引用Alejadro的答案,正确的方法是:

products.sort((a,b) => a.name > b.name ? 1 : -1)


8

我一直为此感到困扰,所以最终我研究了这个问题,并给出了这个冗长的解释,说明事情为什么会是这样。

规范中可以看到:

Section 11.9.4   The Strict Equals Operator ( === )

The production EqualityExpression : EqualityExpression === RelationalExpression
is evaluated as follows: 
- Let lref be the result of evaluating EqualityExpression.
- Let lval be GetValue(lref).
- Let rref be the result of evaluating RelationalExpression.
- Let rval be GetValue(rref).
- Return the result of performing the strict equality comparison 
  rval === lval. (See 11.9.6)

所以现在我们进入11.9.6。
11.9.6   The Strict Equality Comparison Algorithm

The comparison x === y, where x and y are values, produces true or false. 
Such a comparison is performed as follows: 
- If Type(x) is different from Type(y), return false.
- If Type(x) is Undefined, return true.
- If Type(x) is Null, return true.
- If Type(x) is Number, then
...
- If Type(x) is String, then return true if x and y are exactly the 
  same sequence of characters (same length and same characters in 
  corresponding positions); otherwise, return false.

这就是全部。三个等号运算符应用于字符串时,仅当参数完全相同(长度相同且相应位置上的字符相同)时才返回 true。
因此,在比较可能来自不同来源但最终值将相同的字符串时,使用“===”将起作用——这对我们代码中的内联字符串来说是常见情况。例如,如果我们有一个名为“connection_state”的变量,并且我们希望知道以下状态之一“['connecting', 'connected', 'disconnecting', 'disconnected']”现在处于哪个状态,我们可以直接使用“===”。
但还有更多。在11.9.4的正上方,有一个简短的注释:
NOTE 4     
  Comparison of Strings uses a simple equality test on sequences of code 
  unit values. There is no attempt to use the more complex, semantically oriented
  definitions of character or string equality and collating order defined in the 
  Unicode specification. Therefore Strings values that are canonically equal
  according to the Unicode standard could test as unequal. In effect this 
  algorithm assumes that both Strings are already in normalized form.

嗯,现在怎么办?外部获得的字符串可能会很奇怪,其中包含许多Unicode字符,而我们温柔的===操作符可能无法处理它们。这时就需要用到localeCompare函数来解决问题:

15.5.4.9   String.prototype.localeCompare (that)
    ...
    The actual return values are implementation-defined to permit implementers 
    to encode additional information in the value, but the function is required 
    to define a total ordering on all Strings and to return 0 when comparing
    Strings that are considered canonically equivalent by the Unicode standard. 

我们现在可以回家了。
简而言之:为了比较JavaScript中的字符串,请使用localeCompare;如果您知道这些字符串没有非ASCII组件,因为它们是内部程序常量,则也可以使用===。

5

如果您想控制区域设置(或大小写或重音符号),那么请使用Intl.collator

const collator = new Intl.Collator();
list.sort((a, b) => collator.compare(a.attr, b.attr));

您可以像这样构造一个排序器:

new Intl.Collator("en");
new Intl.Collator("en", {sensitivity: "case"});
...

请参见上面的链接以获取文档。
注意:与其他一些解决方案不同,它按照JavaScript方式处理null, undefined,即将它们移到最后。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接