Le premier sera le dernier avec des tableaux JavaScript

Ainsi sera le dernier [0], et le premier [longueur - 1] .– Adapté de Matthieu 20:16

Je vais sauter la Catastrophe Malthusienne et y arriver: les tableaux sont l'une des structures de données les plus simples et les plus importantes. Bien que les éléments terminaux (premier et dernier) soient fréquemment consultés, Javascript ne fournit aucune propriété ou méthode pratique pour le faire et l'utilisation d'indices peut être redondante et sujette à des effets secondaires et des erreurs ponctuelles.

Une proposition JavaScript TC39 récente et moins connue offre un réconfort sous la forme de deux «nouvelles» propriétés: Array.lastItem& Array.lastIndex.

Tableaux Javascript

Dans de nombreux langages de programmation, y compris Javascript, les tableaux sont indexés à zéro. Les éléments terminaux - premier et dernier - sont accessibles via les index [0]et [length — 1], respectivement. Nous devons ce plaisir à un précédent établi par C, où un index représente un décalage par rapport à la tête d'un tableau. Qui fait zéro le premier indice , car il est la tête de tableau. Dijkstra a également proclamé «zéro comme nombre le plus naturel». Alors laissez-le être écrit. Alors laissez-le faire.

Je soupçonne que si vous calculiez l'accès en moyenne par index, vous constateriez que les éléments terminaux sont le plus souvent référencés. Après tout, les tableaux sont couramment utilisés pour stocker une collection triée et, ce faisant, placent les éléments superlatifs (le plus élevé, le plus bas, le plus ancien, le plus récent, etc.) aux extrémités.

Contrairement à d'autres langages de script (par exemple PHP ou Elixir), Javascript ne fournit pas un accès pratique aux éléments du tableau de terminaux. Prenons un exemple trivial de permutation des derniers éléments dans deux tableaux:

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

La logique de permutation nécessite 2 tableaux référencés 8 fois sur 3 lignes! Dans le code du monde réel, cela peut rapidement devenir très répétitif et difficile à analyser pour un humain (bien qu'il soit parfaitement lisible pour une machine).

De plus, en utilisant uniquement des indices, vous ne pouvez pas définir un tableau et obtenir le dernier élément de la même expression. Cela peut ne pas sembler important, mais considérons un autre exemple où la fonction getLogins(),, effectue un appel d'API asynchrone et renvoie un tableau trié. En supposant que nous voulons l'événement de connexion le plus récent à la fin du tableau:

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

À moins que la longueur est fixe et connu à l' avance, nous avons pour attribuer le tableau à une variable locale pour accéder au dernier élément. Une façon courante de résoudre ce problème dans des langages comme Python et Ruby consiste à utiliser des indices négatifs. Ensuite, [length - 1]peut être raccourci en [-1], supprimant le besoin de référence locale.

Je -1ne trouve que marginalement plus lisible que length — 1, et bien qu'il soit possible d'approcher des indices de tableau négatifs en Javascript avec ES6 Proxy ou Array.slice(-1)[0], les deux ont des implications de performances significatives pour ce qui devrait autrement constituer un simple accès aléatoire.

Underscore et Lodash

L'un des principes les plus connus du développement logiciel est Don't Repeat Yourself (DRY). Puisque l'accès aux éléments de terminal est si courant, pourquoi ne pas écrire une fonction d'assistance pour le faire? Heureusement, de nombreuses bibliothèques comme Underscore et Lodash fournissent déjà des utilitaires pour _.first& _.last.

Cela offre une grande amélioration dans l' lastLogin()exemple ci-dessus:

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

Mais quand il s'agit de l'exemple d'échange des derniers éléments, l'amélioration est moins significative:

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

Ces fonctions utilitaires ont supprimé 2 des 8 références, ce n'est que maintenant que nous avons introduit une dépendance externe qui, curieusement, n'inclut pas de fonction pour définir les éléments terminaux.

Très probablement, une telle fonction est délibérément exclue car son API serait déroutante et difficile à lire. Les premières versions de Lodash fournissaient une méthode _.last(array, n)n était le nombre d'éléments à partir de la fin, mais elle a finalement été supprimée en faveur de _.take(array, n).

En supposant qu'il numsexiste un tableau de nombres, quel serait le comportement attendu de _.last(nums, n)? Il pourrait renvoyer les deux derniers éléments comme _.take, ou il pourrait définir la valeur du dernier élément égale à n .

Si nous devions écrire une fonction pour définir le dernier élément d'un tableau, il n'y a que quelques approches à envisager d'utiliser des fonctions pures, le chaînage de méthodes ou l'utilisation d'un prototype:

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

Je ne trouve aucune de ces approches comme étant une grande amélioration. En fait, quelque chose d'important est perdu ici. Chacun effectue une affectation, mais aucun n'utilise l'opérateur d'affectation ( =). Cela pourrait être rendu plus apparent avec des conventions de dénomination comme getLastet setFirst, mais cela devient rapidement trop détaillé. Sans oublier que le cinquième cercle de l'enfer regorge de programmeurs obligés de naviguer dans du code hérité «auto-documenté» où le seul moyen d'accéder ou de modifier les données est d'utiliser des getters et des setters.

D'une manière ou d'une autre, on dirait que nous sommes coincés avec [0]& [length — 1]

Ou sommes-nous? ?

La proposition

Comme mentionné, une proposition de candidat technique ECMAScript (TC39) tente de résoudre ce problème en définissant deux nouvelles propriétés sur l' Arrayobjet: lastItem& lastIndex. Cette proposition est déjà prise en charge dans core-js 3 et utilisable aujourd'hui dans Babel 7 et TypeScript. Même si vous n'utilisez pas de transpileur, cette proposition comprend un polyfill.

Personnellement, je ne trouve pas beaucoup de valeur lastIndexet je préfère la dénomination plus courte de Ruby pour firstet last, bien que cela ait été exclu en raison de problèmes potentiels de compatibilité Web. Je suis également surpris que cette proposition ne suggère pas de firstItempropriété de cohérence et de symétrie.

En attendant, je peux proposer une approche Ruby-esque sans dépendance dans ES6:

Premier Dernier

Nous avons maintenant deux nouvelles propriétés Array - first& last- et une solution qui:

✓ Utilise l'opérateur d'affectation

✓ Ne clone pas la baie

✓ Peut définir un tableau et obtenir un élément terminal dans une expression

✓ Est lisible par l'homme

✓ Fournit une interface pour obtenir et configurer

Nous pouvons réécrire à lastLogin()nouveau en une seule ligne:

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

Mais la vraie victoire vient lorsque nous échangeons les derniers éléments dans deux tableaux avec la moitié du nombre de références:

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

Tout est parfait et nous avons résolu l'un des problèmes les plus difficiles de CS. Il n'y a pas d'alliances perverses cachées dans cette approche ...

Paranoïa prototype

Il n'y a certainement aucun [programmeur] sur terre assez juste pour faire le bien sans jamais pécher. - Adapté d'Ecclésiaste 7:20

Beaucoup considèrent l'extension du prototype d'un Object natif comme un anti-pattern et un crime passible de 100 ans de programmation en Java. Avant l'introduction de la enumerablepropriété, l'extension Object.prototypepouvait modifier le comportement des for inboucles. Cela peut également conduire à des conflits entre diverses bibliothèques, frameworks et dépendances tierces.

Le problème le plus insidieux est peut-être que, sans outils de compilation, une simple faute d'orthographe pourrait créer par inadvertance un tableau associatif.

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

Cette préoccupation n'est pas propre à notre approche, elle s'applique à tous les prototypes Object natifs (y compris les tableaux). Pourtant, cela offre une sécurité sous une forme différente. Les tableaux en Javascript ne sont pas de longueur fixe et par conséquent, il n'y en a pas IndexOutOfBoundsExceptions. L'utilisation Array.lastgarantit que nous n'essayons pas accidentellement d'accéder [length]et de pénétrer involontairement sur le undefinedterritoire.

Quelle que soit l'approche que vous adoptez, il y a des pièges. Une fois de plus, le logiciel se révèle être un art de faire des compromis.

En continuant avec la référence biblique étrangère, en supposant que nous ne croyons pas que l'extension Array.prototypeest un péché éternel, ou que nous sommes prêts à prendre une bouchée du fruit défendu, nous pouvons utiliser cette syntaxe concise et lisible aujourd'hui!

Derniers mots

Les programmes doivent être écrits pour que les gens puissent les lire, et seulement accessoirement pour que les machines s'exécutent. - Harold Abelson

Dans les langages de script comme Javascript, je préfère un code fonctionnel, concis et lisible. Quand il s'agit d'accéder aux éléments du tableau de terminaux, je trouve que la Array.lastpropriété est la plus élégante. Dans une application frontale de production, je pourrais préférer Lodash pour minimiser les conflits et les problèmes entre navigateurs. Mais dans les services back-end Node où je contrôle l'environnement, je préfère ces propriétés personnalisées.

Je ne suis certainement pas le premier, et je ne serai pas le dernier, à apprécier la valeur (ou la mise en garde quant aux implications) de propriétés telles que Array.lastItem, qui, espérons-le, arrivera bientôt dans une version d'ECMAScript près de chez vous.

Suivez-moi sur LinkedIn · GitHub · Moyen