Des indices de tableaux négatifs grâce aux proxies
Par Delicious Insights • Publié le 22 mai 2020

Les meilleures choses ont une fin : voici le dernier article de notre série quotidienne « 19 pépites de JS pur ». On finit en beauté avec une utilisation sympathique des proxies, cette fonctionnalité éblouissante d’ES2015 : autoriser les index négatifs dans les tableaux.

Dans la série…

Extrait de la liste des articles :

  1. La boucle for-of : s’il ne devait en rester qu’une…
  2. Simuler une classe abstraite avec new.target
  3. Des indices de tableaux négatifs grâce aux proxies (cet article)

« Proxy » ?!

Oui, oui, calme-toi. Rien à voir avec les proxies réseau. Je sais combien les PALC (Proxy À La Con™) d’entreprise constituent une expérience traumatisante…

Un proxy, c’est par définition un intermédiaire. Et là c’est exactement ça : un objet qui intercepte toutes les manipulations possibles d’un autre, et choisit au cas par cas de laisser passer, altérer, interdire…

Point très important : un proxy n’altère jamais l’objet d’origine : c’est un enrobage de cet objet, qui n’empêche en rien ton code d’utiliser l’original directement s’il a une référence dessus. L’idée est de transmettre à du code tiers, lorsqu’on le souhaite, uniquement la référence du proxy.

Les tableaux et les indices négatifs

Pour rappel, les indices négatifs partent de la fin : -1 est le dernier élément, -2 l’avant-dernier, etc. Super pratique.

L’API de Array autorise les indices négatifs :

  • slice(from, to) permet des valeurs négatives.
  • splice(from, count[, ...items]) permet un from négatif.

Malheureusement, la sémantique générale de l’opérateur d’indexation indirecte d’un objet, […], impose que la propriété dont le nom est évalué entre les crochets existe avec ce nom-là. Et les propriétés numériques des tableaux ne sont pas négatives.

const fibo = [1, 1, 2, 3, 5, 8, 13]
fibo.slice(-3, -1) // => [5, 8]
fibo.splice(-3, 3) // => [5, 8, 13]
fibo[3] // => 3
fibo[-1] // => undefined 😢
fibo[-1] = 4
fibo // => [1, 1, 2, 3, '-1': 14] 😭

Ça manque grave, non ? Alors ajoutons-les !

It’s a trap!

Un proxy est défini sur la base de deux choses :

  1. Une cible (target) : l’objet d’origine, qu’on va enrober
  2. Un gestionnaire (handler), qui est un simple objet doté de certaines méthodes prédéfinies, appelées « trappes » (traps). Un gestionnaire vide revient donc à ne rien modifier, ce qui rend le proxy superflu…

Le langage définit une trappe par interaction possible avec un objet. On a par exemple has, qui intercepte l’opérateur in de test d’existence de propriété, ou apply, qui intercepte, pour les objets fonctions, le fait de les appeler (avec l’opérateur (…)).

La syntaxe générale est simple :

const result = new Proxy(target, {
  someTrap() {},
  someOtherTrap() {},
})

Nous, on va s’intéresser aux trappes get et set, qui interceptent la lecture et l’écriture de propriété. On ne poussera pas le bouchon jusqu’à assurer une cohérence intégrale via notamment les trappes has, ownKeys et deleteProperty, parce qu’en vrai, on manipule rarement des tableaux autrement que par indexation ou par leur API dédiée. Mais si tu veux t’amuser, n’hésite pas…

Implémenter la lecture

OK, commençons par la lecture. L’idée est simple :

  1. On prend le nom de la propriété demandée (ce nom sera, techniquement, une String ou un Symbol, qui sont les deux seuls types de noms de propriété autorisés par le langage).
  2. Si le nom représente un entier négatif (ce qu’on ne pourra pas tester sur un symbole, donc gaffe), on le convertit en son équivalent positif en…
    1. en faisant un véritable Number
    2. l’ajoutant à length
  3. Pour finir, on délègue à l’implémentation native de la lecture de propriété.

Et comment fait-on ça, justement ? La meilleure pratique pour les proxies consiste à utiliser l’API Reflect, apparue en même temps, qui fournit un accès plutôt bas niveau à chaque interaction native correspondante aux trappes. Ainsi, pour notre trappe get, on utiliserait Reflect.get, qui a exactement la même signature.

Voyons ça :

function makeArrayNegativeFriendly(array) {
  return new Proxy(array, {
    get(target, prop, receiver) {
      if (typeof prop === 'string' && Number(prop) < 0) {
        prop = target.length + Number(prop)
      }
      return Reflect.get(target, prop, receiver)
    },
  })
}

const fibo = [1, 1, 2, 3, 5, 8, 13]
const niceFibo = makeArrayNegativeFriendly(fibo)
niceFibo[6] // => 13
niceFibo[-1] // => 13 🎉😍

Elle est pas belle la vie ?!

Implémenter l’écriture

OK, pour l’écriture on fait exactement pareil, mais pour la trappe set :

function makeArrayNegativeFriendly(array) {
  return new Proxy(array, {
    get(target, prop, receiver) {
      /* … */
    },
    set(target, prop, value, receiver) {
      if (typeof prop === 'string' && Number(prop) < 0) {
        prop = target.length + Number(prop)
      }
      return Reflect.set(target, prop, value, receiver)
    },
  })
}

const fibo = [1, 1, 2, 3, 5, 8, 13]
const niceFibo = makeArrayNegativeFriendly(fibo)
niceFibo[-1] = 14
niceFibo[6] // => 14 🎉😍
fibo[6] // => 14 🎉😍

Et voilà le travail !

C’est dispo où ?

Les proxies sont natifs à partir de Chrome 49, Firefox 18, Opera 36, Edge 12, Safari 10 et Node 6.

En revanche, contrairement à ce que je vous dis d’habitude, ça ne se transpile pas. Ce n’est tout simplement pas simulable en ES5. Donc soit c’est natif, soit il faut ruser de fou à coup d’accesseurs dans les descripteurs de propriété, ce qui est plus lourd, plus lent, et surtout pas du tout dynamique (il faut que les propriétés soient connues et enrobées à l’avance, ce qui dans notre cas serait une pure tannée).

Envie d’en savoir plus (sur les proxies) ?

Si le sujet vous botte, je l’ai exploré bien en détail (et avec plein d’exemples marrants, utiles ou les deux) dans une présentation que j’ai donnée, entre autres, à Fronteers 2019 (les slides sont ici).

Notre super formation de dingues ES Total les explore également en profondeur.

Envie d’en savoir plus (en général) ?

Nos formations envoient du gros pâté, en présentiel ou à distance (FOAD), en inter-entreprises ou en intra rien que pour ta boîte ! Qui plus est, pendant la crise du Covid-19, les salarié·e·s peuvent être formé·e·s gratuitement ! Ce serait vraiment trop bête de ne pas en profiter !

That’s a wrap!

Pfiou ! Et voilà, 19 jours, 19 articles sur des « pépites » JavaScript. J’espère que le format vous a plu, que vous avez souri, appris des trucs, halluciné, et toutes ces choses. N’hésitez pas à en parler sur Twitter !

De prochaines séries sont prévues bientôt, notamment sur les nouveautés d’ES2020 et des « pépites » sur Node.js (cœur et modules noyaux, aucun code tiers). Gardez-nous à l’œil !

Découvrez notre cours vidéo : JavaScript : this is it ! 🖥

Tout savoir sur le fonctionnement de this en JavaScript, des règles fondamentales aux ajustements des API, en passant par les fonctions fléchées, le binding et bien plus encore…