Level up your .sort game

Avatar of Adam Giese
Adam Giese am

DigitalOcean bietet Cloud-Produkte für jede Phase Ihrer Reise. Starten Sie mit 200 $ kostenlosem Guthaben!

Sortieren ist eine super praktische JavaScript-Methode, mit der die Werte eines Arrays in einer bestimmten Reihenfolge angezeigt werden können. Ob Immobilienangebote nach Preis, Burgerläden nach Entfernung oder die besten Happy Hours in der Nähe nach Bewertung – das Sortieren von Informationsarrays ist ein häufiges Bedürfnis.

Wenn Sie dies bereits mit JavaScript in einem Projekt tun, verwenden Sie wahrscheinlich die integrierte Array-Methode .sort, die zur selben Familie von Array-Methoden gehört wie .filter, .map und .reduce.

Schauen wir uns an, wie das geht!

Eine kurze Anmerkung zu Seiteneffekten

Bevor wir auf die Details der Verwendung von .sort eingehen, gibt es ein sehr wichtiges Detail, das angesprochen werden muss. Während viele der ES5-Array-Methoden wie .filter, .map und .reduce ein neues Array zurückgeben und das Original unverändert lassen, sortiert .sort das Array direkt. Wenn dies unerwünscht ist, ist eine ES6-Technik, um dies zu vermeiden, die Verwendung des Spread-Operators, um prägnant ein neues Array zu erstellen.

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 und fooSorted verweisen beide auf dasselbe Array, aber bar und barSorted sind nun einzelne Arrays.

Allgemeiner Überblick

Der einzige Parameter der Methode .sort ist eine Funktion. Die Spezifikation nennt diese compareFn – ich werde sie im Rest des Beitrags als „Vergleichsfunktion“ bezeichnen. Diese Vergleichsfunktion akzeptiert zwei Parameter, die ich a und b nennen werde. a und b sind die beiden Elemente, die wir vergleichen werden. Wenn Sie keine Vergleichsfunktion bereitstellen, konvertiert das Array jedes Element in einen String und sortiert entsprechend den Unicode-Punkten.

Wenn Sie möchten, dass a zuerst im Array geordnet wird, sollte die Vergleichsfunktion eine negative Ganzzahl zurückgeben; für b eine positive Ganzzahl. Wenn Sie möchten, dass die beiden ihre aktuelle Reihenfolge beibehalten, geben Sie 0 zurück.

Wenn Sie das nicht verstehen, machen Sie sich keine Sorgen! Hoffentlich wird es mit ein paar Beispielen viel klarer.

Vergleich von Zahlen

Eine der einfachsten Rückruffunktionen, die man schreiben kann, ist ein Zahlenvergleich.

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]

Wenn a größer als b ist, gibt a - b eine positive Zahl zurück, also wird b zuerst sortiert.

Vergleich von Zeichenketten

Beim Vergleichen von Zeichenketten vergleichen die Operatoren > und < Werte basierend auf dem Unicode-Wert jeder Zeichenkette. Das bedeutet, dass alle Großbuchstaben „kleiner“ sind als alle Kleinbuchstaben, was zu unerwartetem Verhalten führen kann.

JavaScript *hat* eine Methode, die beim Vergleichen von Zeichenketten hilft: die Methode String.prototype.localeCompare. Diese Methode akzeptiert eine Vergleichszeichenkette, eine Locale und ein Optionenobjekt. Das Optionenobjekt akzeptiert einige Eigenschaften (alle können Sie hier einsehen), aber ich finde die nützlichste ist „sensitivity“. Diese beeinflusst, wie Vergleiche zwischen Buchstabenvariationen wie Groß-/Kleinschreibung und Akzenten funktionieren.

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" ]
}
*/

Ausführen von Funktionen vor dem Vergleichen von Werten

Möglicherweise möchten Sie eine Vergleichsfunktion auf einen Wert anwenden, der aus jedem Element des Arrays abgeleitet wird. Zuerst schreiben wir eine Vergleichsfunktionsfabrik, die über das Element „mapped“ (abbildet), bevor sie die Vergleichsfunktion aufruft.

const sortByMapped = (map,compareFn) => (a,b) => compareFn(map(a),map(b));

Ein Anwendungsfall dafür ist das Sortieren basierend auf einem Attribut eines Objekts.

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 }
]
*/

Ein weiterer Fall könnte das Vergleichen eines Datumsarrays sein.

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"]

Umkehren einer Sortierung

Es gibt Fälle, in denen Sie das Ergebnis einer Vergleichsfunktion umkehren möchten. Dies unterscheidet sich subtil vom Sortieren und anschließenden Umkehren des Ergebnisses in Bezug auf die Behandlung von Gleichständen: Wenn Sie das Ergebnis umkehren, werden auch Gleichstände in der Reihenfolge umgekehrt.

Um eine höherrangige Funktion zu schreiben, die eine Vergleichsfunktion akzeptiert und eine neue zurückgibt, müssen Sie das Vorzeichen des Rückgabewerts des Vergleichs ändern.

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']

Ausführen einer „Tie-Breaker“-Sortierung

Es gibt Zeiten, in denen Sie eine „Tie-Breaker“-Sortierung wünschen – das heißt, eine weitere Vergleichsfunktion, die im Falle eines Gleichstands verwendet wird.

Durch die Verwendung von [].reduce können Sie ein Array von Vergleichsfunktionen zu einer einzigen zusammenführen.

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}
*/

Schreiben einer zufälligen Sortierung

Möglicherweise möchten Sie ein Array „zufällig“ sortieren. Eine Technik, die ich gesehen habe, ist die Verwendung der folgenden Funktion als Vergleichsfunktion.

const byRandom = () => Math.random() - .5;

Da Math.random() eine „zufällige“ Zahl zwischen 0 und 1 zurückgibt, sollte die Funktion byRandom die Hälfte der Zeit eine positive Zahl und die andere Hälfte eine negative Zahl zurückgeben. Das scheint eine gute Lösung zu sein, aber leider, da die Vergleichsfunktion nicht „konsistent“ ist – was bedeutet, dass sie möglicherweise nicht denselben Wert zurückgibt, wenn sie mehrmals mit denselben Werten aufgerufen wird –, kann dies zu einigen unerwarteten Ergebnissen führen.

Nehmen wir zum Beispiel ein Array von Zahlen zwischen 0 und 4. Wenn diese byRandom-Funktion wirklich zufällig wäre, würde man erwarten, dass der neue Index jeder Zahl bei genügend Iterationen gleichmäßig über die Indizes verteilt ist. Der ursprüngliche Wert 0 hätte im neuen Array genauso wahrscheinlich den Index 4 wie den Index 0. In der Praxis wird diese Funktion jedoch jede Zahl auf ihre ursprüngliche Position schieben.

Siehe den Pen
Array.sort() Random 👎
von Adam Giese (@AdamGiese)
auf CodePen.

Die „Diagonale“ von links oben hat statistisch den größten Wert. Bei einer idealen und wirklich zufälligen Sortierung würden die meisten Tabellenzellen bei etwa 20 % liegen.

Die Lösung dafür ist, einen Weg zu finden, um sicherzustellen, dass die Vergleichsfunktion konsistent bleibt. Eine Möglichkeit besteht darin, den Zufallswert jeder Array-Element vor dem Vergleich zuzuordnen und ihn danach wieder zu entfernen.

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);

Dies stellt sicher, dass jedes Element einen einzelnen Zufallswert hat, der nur einmal pro Element berechnet wird und nicht einmal pro Vergleich. Dies beseitigt die Sortierverzerrung zur ursprünglichen Position.

Siehe den Pen
Array.sort() Random 👍
von Adam Giese (@AdamGiese)
auf CodePen.