排序是一个非常方便的 JavaScript 方法,可以按特定顺序显示数组的值。无论是按价格排序房地产列表,按距离排序汉堡店,还是按评分排序附近最佳欢乐时光,排序信息数组都是常见的需求。
如果您已经在项目中使用 JavaScript 进行此操作,那么您可能正在使用内置的数组 .sort
方法,它与包括 .filter
、.map
和 .reduce
在内的其他数组方法属于同一类。
让我们来看看如何做到这一点!
关于副作用的快速说明
在深入探讨如何使用 .sort
的细节之前,需要解决一个非常重要的细节。虽然许多 ES5 数组方法(例如 .filter
、.map
和 .reduce
)会返回一个新数组并保持原始数组不变,但 .sort
会就地排序数组。如果不需要这样做,则可以使用 ES6 技术来避免这种情况,即使用扩展运算符简洁地创建一个新数组。
const foo = ['c','b','a'];
const bar = ['x','z','y'];
const fooSorted = foo.sort();
const barSorted = [...bar].sort();
console.log({foo, fooSorted, bar, barSorted});
/*
{
"foo": [ "a", "b", "c" ],
"fooSorted": [ "a", "b", "c" ],
"bar": [ "x", "z", "y" ],
"barSorted": [ "x", "y", "z" ]
}
*/
foo
和 fooSorted
都引用同一个数组,但 bar
和 barSorted
现在是单独的数组。
概述
.sort
方法的唯一参数是函数。规范 将其称为 compareFn
——在本文的其余部分,我将将其称为“比较函数”。此比较函数接受两个参数,我将分别称为 a
和 b
。a
和 b
是我们将要比较的两个元素。如果您不提供比较函数,则数组会将每个元素强制转换为字符串并根据 Unicode 代码点 进行排序。
如果您希望 a
首先出现在数组中,则比较函数应返回一个负整数;对于 b
,则返回一个正整数。如果您希望两者保持其当前顺序,则返回 0
。
如果您不理解,别担心!希望通过几个例子,它会变得更加清晰。
比较数字
最简单的回调之一是数字比较。
const numbers = [13,8,2,21,5,1,3,1];
const byValue = (a,b) => a - b;
const sorted = [...numbers].sort(byValue);
console.log(sorted); // [1,1,2,3,5,8,13,21]
如果 a
大于 b
,则 a - b
将返回一个正数,因此 b
将首先排序。
比较字符串
比较字符串时,>
和 <
运算符将根据每个字符串的 Unicode 值进行比较。这意味着所有大写字母都将“小于”所有小写字母,这可能导致意外行为。
JavaScript确实有一种方法可以帮助比较字符串:String.prototype.localeCompare
方法。此方法接受一个比较字符串、一个区域设置和一个选项对象。选项对象接受一些属性(所有这些属性您都可以在 此处 查看),但我发现最有用的是“敏感性”。这将影响大小写和重音等字母变体之间的比较方式。
const strings = ['Über', 'alpha', 'Zeal', 'über', 'uber', 'Uber', 'Alpha', 'zeal'];
const sortBySensitivity = sensitivity => (a, b) => a.localeCompare(
b,
undefined, // locale string -- undefined means to use browser default
{ sensitivity }
);
const byAccent = sortBySensitivity('accent');
const byBase = sortBySensitivity('base');
const byCase = sortBySensitivity('case');
const byVariant = sortBySensitivity('variant'); // default
const accentSorted = [...strings].sort(byAccent);
const baseSorted = [...strings].sort(byBase);
const caseSorted = [...strings].sort(byCase);
const variantSorted = [...strings].sort(byVariant);
console.log({accentSorted, baseSorted, caseSorted, variantSorted});
/*
{
"accentSorted": [ "alpha", "Alpha", "uber", "Uber", "Über", "über", "Zeal", "zeal" ],
"baseSorted": [ "alpha", "Alpha", "Über", "über", "uber", "Uber", "Zeal", "zeal" ],
"caseSorted": [ "alpha", "Alpha", "über", "uber", "Über", "Uber", "zeal", "Zeal" ],
"variantSorted": [ "alpha", "Alpha", "uber", "Uber", "über", "Über", "zeal", "Zeal" ]
}
*/
比较值之前运行函数
您可能希望对从每个数组元素派生的值运行比较函数。首先,让我们编写一个比较函数工厂,它将在调用比较函数之前“映射”元素。
const sortByMapped = (map,compareFn) => (a,b) => compareFn(map(a),map(b));
此功能的一个用例是根据对象的属性进行排序。
const purchases = [
{ name: 'Popcorn', price: 5.75 },
{ name: 'Movie Ticket', price: 12 },
{ name: 'Soda', price: 3.75 },
{ name: 'Candy', price: 5 },
];
const sortByMapped = (map,compareFn) => (a,b) => compareFn(map(a),map(b));
const byValue = (a,b) => a - b;
const toPrice = e => e.price;
const byPrice = sortByMapped(toPrice,byValue);
console.log([...purchases].sort(byPrice));
/*
[
{ name: "Soda", price: 3.75 },
{ name: "Candy", price: 5 },
{ name: "Popcorn", price: 5.75 },
{ name: "Movie Ticket", price: 12 }
]
*/
另一种情况可能是比较日期数组。
const dates = ['2018-12-10', '1991-02-10', '2015-10-07', '1990-01-11'];
const sortByMapped = (map,compareFn) => (a,b) => compareFn(map(a),map(b));
const toDate = e => new Date(e).getTime();
const byValue = (a,b) => a - b;
const byDate = sortByMapped(toDate,byValue);
console.log([...dates].sort(byDate));
// ["1990-01-11", "1991-02-10", "2015-10-07", "2018-12-10"]
反转排序
在某些情况下,您可能希望反转比较函数的结果。这与进行排序然后反转结果的方式在处理平局方面略有不同:如果反转结果,平局的顺序也将被反转。
要编写一个接受比较函数并返回新函数的高阶函数,您需要翻转比较返回值的符号。
const flipComparison = fn => (a,b) => -fn(a,b);
const byAlpha = (a,b) => a.localeCompare(b, null, { sensitivity: 'base' });
const byReverseAlpha = flipComparison(byAlpha);
console.log(['A', 'B', 'C'].sort(byReverseAlpha)); // ['C','B','A']
运行决胜局排序
有时您可能希望进行“决胜局”排序——即在平局情况下使用的另一个比较函数。
通过使用 [].reduce
,您可以将比较函数数组展平为单个函数。
const sortByMapped = map => compareFn => (a,b) => compareFn(map(a),map(b));
const flipComparison = fn => (a,b) => -fn(a,b);
const byValue = (a,b) => a - b;
const byPrice = sortByMapped(e => e.price)(byValue);
const byRating = sortByMapped(e => e.rating)(flipComparison(byValue));
const sortByFlattened = fns => (a,b) =>
fns.reduce((acc, fn) => acc || fn(a,b), 0);
const byPriceRating = sortByFlattened([byPrice,byRating]);
const restaurants = [
{ name: "Foo's Burger Stand", price: 1, rating: 3 },
{ name: "The Tapas Bar", price: 3, rating: 4 },
{ name: "Baz Pizza", price: 3, rating: 2 },
{ name: "Amazing Deal", price: 1, rating: 5 },
{ name: "Overpriced", price: 5, rating: 1 },
];
console.log(restaurants.sort(byPriceRating));
/*
{name: "Amazing Deal", price: 1, rating: 5}
{name: "Foo's Burger Stand", price: 1, rating: 3}
{name: "The Tapas Bar", price: 3, rating: 4}
{name: "Baz Pizza", price: 3, rating: 2}
{name: "Overpriced", price: 5, rating: 1}
*/
编写随机排序
您可能希望“随机”排序数组。我见过的一种技术是使用以下函数作为比较函数。
const byRandom = () => Math.random() - .5;
由于 Math.random()
返回 0
到 1
之间的“随机”数,因此 byRandom
函数应该有一半时间返回正数,另一半时间返回负数。这似乎是一个不错的解决方案,但不幸的是,由于比较函数不“一致”——这意味着当使用相同的值多次调用它时,它可能不会返回相同的值——它可能会导致一些意外的结果。
例如,让我们取一个 0 到 4 之间的数字数组。如果此 byRandom
函数确实是随机的,则预期每个数字的新索引在足够多的迭代中将均匀地分布在整个数组中。原始的 0 值在新数组中处于索引 4 的可能性与处于索引 0 的可能性相同。但是,在实践中,此函数会使每个数字偏向其原始位置。
查看 CodePen 上 Adam Giese 编写的
Array.sort() 随机 👎
在 CodePen 上。
左上方的“对角线”在统计上将具有最大的值。在理想的真正随机排序中,每个表格单元格都将在 20% 左右徘徊。
解决此问题的办法是找到一种方法来确保比较函数保持一致。一种方法是在比较之前将随机值映射到每个数组元素,然后在比较之后将其映射回去。
const sortByMapped = map => compareFn => (a,b) => compareFn(map(a),map(b));
const values = [0,1,2,3,4,5,6,7,8,9];
const withRandom = (e) => ({ random: Math.random(), original: e });
const toOriginal = ({original}) => original;
const toRandom = ({random}) => random;
const byValue = (a,b) => a - b;
const byRandom = sortByMapped(toRandom)(byValue);
const shuffleArray = array => array
.map(withRandom)
.sort(byRandom)
.map(toOriginal);
这确保每个元素只有一个随机值,该值每个元素仅计算一次,而不是每次比较都计算一次。这消除了对原始位置的排序偏差。
查看 CodePen 上 Adam Giese 编写的
Array.sort() 随机 👍
在 CodePen 上。
等等,为什么基本排序是最合乎逻辑的?那个破坏了大小写连续性。IMO,大小写和变体排序似乎更合乎逻辑。
公平地说,作者确实说“对我来说”,我同意这一点,但这都取决于上下文,不是吗?
例如,如果我正在浏览汽车型号名称,我宁愿扫描基本排序的列表以查找我需要的信息。但在另一种情况下,大小写排序可能更有意义。
这正是我所想的,尽管这仅仅是一个观点。
这是一篇内容丰富的文章。我编写了 compose-sort npm 库来帮助解决“断路器”行为的复杂性问题。我只是想指出它,以防它对任何想要避免编写上面列出的函数的人有所帮助。
不错的文章。关于“比较字符串”,据我所知,当您有一个想要排序的大型字符串数组时,最好(性能方面)使用
Intl.Collator
及其compare
方法。至于“随机排序”,它似乎过于复杂,并且听起来更像是使用随机索引进行洗牌,而不是随机排序。我是否错过了什么?
我必须说,我非常喜欢你的箭头游戏。一开始阅读起来并不直观——但一旦我习惯了,它看起来非常清晰。
” ‘ü’, ‘u’, ‘Ü’, 和 ‘U’ 是等价的”
作为母语中存在字母“ü”的人,我发现这句话有点无知,而且语法上不正确。
u 和 ü 在字母表中都有自己的位置作为独立的字母,它们的位置也决定了顺序。
在匈牙利语(和德语)中,ü 在 u 之后,虚构的单词“üa”肯定在“uz”之后。
对不起——我的意思是它们在基本排序中是等价的,而不是在语言中等价,但我意识到我写的方式不太清楚。
我完全删除了这句话,因为它对文章来说没有必要。