Der erste soll der letzte mit JavaScript-Arrays sein

So soll das Letzte sein [0]und das Erste [Länge - 1]. - Nach Matthäus 20, 16

Ich werde die malthusianische Katastrophe überspringen und darauf eingehen: Arrays sind eine der einfachsten und wichtigsten Datenstrukturen. Während häufig auf Terminalelemente (erstes und letztes) zugegriffen wird, bietet Javascript keine bequeme Eigenschaft oder Methode, um dies zu tun, und die Verwendung von Indizes kann redundant und anfällig für Nebenwirkungen und Fehler nacheinander sein.

Ein weniger bekannter, aktueller JavaScript TC39-Vorschlag bietet Trost in Form von zwei „neuen“ Eigenschaften: Array.lastItem& Array.lastIndex.

Javascript Arrays

In vielen Programmiersprachen, einschließlich Javascript, sind Arrays nullindiziert. Die Anschlußelemente-erste und letzt werden über die zugegriffen wird, [0]und [length — 1]Indizes, respectively. Wir verdanken diese Freude einem Präzedenzfall, der durch C festgelegt wurde, wobei ein Index einen Versatz vom Kopf eines Arrays darstellt. Das macht Null den ersten Index , weil es ist der Array Kopf. Auch Dijkstra proklamierte "Null als natürlichste Zahl". Also lass es geschrieben werden. Also lass es geschehen.

Ich vermute, wenn Sie den Zugriff nach Index gemittelt haben, werden Sie feststellen, dass Terminalelemente am häufigsten referenziert werden. Schließlich werden Arrays häufig zum Speichern einer sortierten Sammlung verwendet, wobei Elemente der Superlative (höchste, niedrigste, älteste, neueste usw.) an den Enden platziert werden.

Im Gegensatz zu anderen Skriptsprachen (z. B. PHP oder Elixir) bietet Javascript keinen bequemen Zugriff auf Terminal-Array-Elemente. Stellen Sie sich ein einfaches Beispiel für das Austauschen der letzten Elemente in zwei Arrays vor:

let faces = ["?", "?", "?", "?", "?"];let animals = ["?", "?", "?", "?", "?"]; 
let lastAnimal = animals[animals.length - 1];animals[animals.length - 1] = faces[faces.length - 1];faces[faces.length - 1] = lastAnimal;

Die Austauschlogik erfordert 2 Arrays, auf die 8 Mal in 3 Zeilen verwiesen wird! Im realen Code kann sich dies schnell wiederholen und für einen Menschen schwer zu analysieren sein (obwohl es für eine Maschine perfekt lesbar ist).

Darüber hinaus können Sie nur mithilfe von Indizes kein Array definieren und das letzte Element im selben Ausdruck abrufen. Das mag nicht wichtig erscheinen, aber betrachten Sie ein anderes Beispiel, in dem die Funktion getLogins()einen asynchronen API-Aufruf ausführt und ein sortiertes Array zurückgibt. Angenommen, wir möchten das neueste Anmeldeereignis am Ende des Arrays:

let lastLogin = async () => { let logins = await getLogins(); return logins[logins.length - 1];};

Es sei denn , die Länge festgelegt ist und im Voraus bekannt ist , wir haben das Array zu einer lokalen Variablen zuweisen , um das letzte Element zuzugreifen. Ein üblicher Weg, dies in Sprachen wie Python und Ruby anzugehen, ist die Verwendung negativer Indizes. Dann [length - 1]kann verkürzt werden [-1], wodurch die Notwendigkeit für lokale Referenz zu entfernen.

Ich finde -1nur unwesentlich besser lesbar als length — 1und obwohl es möglich ist, negative Array-Indizes in Javascript mit ES6-Proxy zu approximieren Array.slice(-1)[0], haben beide erhebliche Auswirkungen auf die Leistung für das, was sonst einen einfachen Direktzugriff darstellen sollte.

Unterstrich & Lodash

Eines der bekanntesten Prinzipien in der Softwareentwicklung ist Don't Repeat Yourself (DRY). Da der Zugriff auf Terminalelemente so häufig ist, schreiben Sie eine Hilfsfunktion, um dies zu tun. Glücklicherweise bieten viele Bibliotheken wie Underscore und Lodash bereits Dienstprogramme für _.first& an _.last.

Dies bietet eine große Verbesserung im lastLogin()obigen Beispiel:

let lastLogin = async () => _.last(await getLogins());

Wenn es jedoch um das Austauschen der letzten Elemente geht, ist die Verbesserung weniger bedeutend:

let faces = ["?", "?", "?", "?", "?"];let animals = ["?", "?", "?", "?", "?"]; 
let lastAnimal = _.last(animals);animals[animals.length - 1] = _.last(faces);faces[faces.length - 1] = lastAnimal;

Diese Dienstprogrammfunktionen haben 2 der 8 Referenzen entfernt. Erst jetzt haben wir eine externe Abhängigkeit eingeführt, die seltsamerweise keine Funktion zum Festlegen von Terminalelementen enthält.

Höchstwahrscheinlich wird eine solche Funktion absichtlich ausgeschlossen, da ihre API verwirrend und schwer lesbar wäre. Frühere Versionen von Lodash lieferten eine Methode, _.last(array, n)bei der n die Anzahl der Elemente vom Ende war, die jedoch letztendlich zugunsten von entfernt wurde _.take(array, n).

Angenommen, es numshandelt sich um ein Array von Zahlen. Was wäre das erwartete Verhalten _.last(nums, n)? Es könnte die letzten beiden Elemente wie zurückgeben _.takeoder den Wert des letzten Elements auf n setzen .

Wenn wir eine Funktion zum Setzen des letzten Elements in einem Array schreiben möchten, gibt es nur wenige Ansätze, um reine Funktionen, Methodenverkettungen oder Prototypen zu verwenden:

let nums = ['d', 'e', 'v', 'e', 'l']; // set first = last
_.first(faces, _.last(faces)); // Lodash style
$(faces).first($(faces).last()); // jQuery style
faces.first(faces.last()); // prototype

Ich finde keinen dieser Ansätze eine große Verbesserung. Tatsächlich geht hier etwas Wichtiges verloren. Jeder führt eine Zuweisung durch, aber keiner verwendet den Zuweisungsoperator ( =). Dies könnte durch Namenskonventionen wie getLastund deutlicher gemacht werden setFirst, aber das wird schnell zu ausführlich. Ganz zu schweigen davon, dass der fünfte Kreis der Hölle voll von Programmierern ist, die gezwungen sind, durch „selbstdokumentierenden“ Legacy-Code zu navigieren, wobei der einzige Weg, auf Daten zuzugreifen oder diese zu ändern, Getter und Setter sind.

Irgendwie sieht es so aus, als ob wir mit [0]& [length — 1]

Oder sind wir? ?

Der Antrag

Wie bereits erwähnt, versucht ein Vorschlag für einen technischen Kandidaten für ECMAScript (TC39), dieses Problem zu lösen, indem zwei neue Eigenschaften für das ArrayObjekt definiert werden: lastItem& lastIndex. Dieser Vorschlag wird bereits in Core-JS 3 unterstützt und kann heute in Babel 7 und TypeScript verwendet werden. Auch wenn Sie keinen Transpiler verwenden, enthält dieser Vorschlag eine Polyfüllung.

Persönlich finde ich nicht viel Wert in lastIndexund bevorzuge Rubys kürzere Benennung für firstund last, obwohl dies aufgrund möglicher Probleme mit der Webkompatibilität ausgeschlossen wurde. Ich bin auch überrascht, dass dieser Vorschlag keine firstItemEigenschaft für Konsistenz und Symmetrie vorschlägt .

In der Zwischenzeit kann ich in ES6 einen Ruby-ähnlichen Ansatz ohne Abhängigkeit anbieten:

Erste & Letzte

Wir haben jetzt zwei neue Array-Eigenschaften - first& last- und eine Lösung, die:

✓ Verwendet den Zuweisungsoperator

✓ Klont das Array nicht

✓ Kann ein Array definieren und ein Terminalelement in einem Ausdruck erhalten

✓ Ist für Menschen lesbar

✓ Bietet eine Schnittstelle zum Abrufen und Einstellen

Wir können lastLogin()wieder in einer einzigen Zeile schreiben :

let lastLogin = async () => (await getLogins()).last;

Der eigentliche Gewinn kommt jedoch, wenn wir die letzten Elemente in zwei Arrays mit der halben Anzahl von Referenzen austauschen:

let faces = ["?", "?", "?", "?", "?"];let animals = ["?", "?", "?", "?", "?"]; 
let lastAnimal = animals.last;animals.last = faces.last;faces.last = lastAnimal;

Alles ist perfekt und wir haben eines der schwierigsten Probleme von CS gelöst. In diesem Ansatz verstecken sich keine bösen Bündnisse…

Prototyp Paranoia

Sicherlich gibt es niemanden auf der Erde, der so gerecht ist, dass er Gutes tut, ohne jemals zu sündigen (nach Prediger 7:20)

Viele betrachten die Erweiterung des Prototyps eines nativen Objekts als Anti-Muster und Verbrechen, das mit 100 Jahren Programmierung in Java geahndet werden kann. Vor der Einführung der enumerableEigenschaft kann eine Erweiterung Object.prototypedas Verhalten von for inSchleifen ändern . Dies kann auch zu Konflikten zwischen verschiedenen Bibliotheken, Frameworks und Abhängigkeiten von Drittanbietern führen.

Das vielleicht heimtückischste Problem ist, dass ein einfacher Rechtschreibfehler ohne Tools zur Kompilierungszeit versehentlich ein assoziatives Array erzeugen könnte.

let faces = ["?", "?", "?", "?", "?"];let ln = faces.length 
faces.lst = "?"; // (5) ["?", "?", "?", "?", "?", lst: "?"] 
faces.lst("?"); // Uncaught TypeError: faces.lst is not a function 
faces[ln] = "?"; // (6) ["?", "?", "?", "?", "?", "?"] 

Dieses Problem betrifft nicht nur unseren Ansatz, sondern gilt für alle nativen Objektprototypen (einschließlich Arrays). Dies bietet jedoch Sicherheit in einer anderen Form. Arrays in Javascript sind nicht in der Länge festgelegt und folglich gibt es keine IndexOutOfBoundsExceptions. Durch Array.lastdie Verwendung wird sichergestellt, dass wir nicht versehentlich versuchen, auf [length]das undefinedGebiet zuzugreifen und es unbeabsichtigt zu betreten .

Egal welchen Ansatz Sie wählen, es gibt Fallstricke. Wieder einmal erweist sich Software als eine Kunst, Kompromisse einzugehen.

Wenn wir mit der fremden biblischen Referenz fortfahren und davon ausgehen, dass das Erweitern Array.prototypekeine ewige Sünde ist oder wir bereit sind, einen Bissen von der verbotenen Frucht zu nehmen, können wir diese prägnante und lesbare Syntax heute verwenden!

Letzte Worte

Programme müssen geschrieben werden, damit Benutzer sie lesen können, und nur im Übrigen, damit Maschinen ausgeführt werden können. - Harold Abelson

In Skriptsprachen wie Javascript bevorzuge ich Code, der funktional, präzise und lesbar ist. Wenn ich auf Terminal-Array-Elemente zugreifen möchte, finde ich die Array.lastEigenschaft am elegantesten. In einer Produktions-Front-End-Anwendung würde ich möglicherweise Lodash bevorzugen, um Konflikte und browserübergreifende Bedenken zu minimieren. In Node-Back-End-Diensten, in denen ich die Umgebung steuere, bevorzuge ich diese benutzerdefinierten Eigenschaften.

Ich bin sicherlich nicht der Erste und werde auch nicht der Letzte sein, der den Wert (oder die Vorsicht hinsichtlich der Auswirkungen) von Eigenschaften wie schätzt Array.lastItem, was hoffentlich bald zu einer Version von ECMAScript in Ihrer Nähe kommt.

Folgen Sie mir auf LinkedIn · GitHub · Medium