Convertir un objet en Map et réciproquement
Par Delicious Insights • Publié le 19 mai 2020

Voici déjà le seixième article de notre série quotidienne « 19 pépites de JS pur ». On parle aujourd’hui d’un des types de collection apparus avec ES2015 : Map. Quand l’utiliser plutôt qu’un simple objet, et comment passer aisément de l’un à l’autre ?

Dans la série…

Extrait de la liste des articles :

  1. Utiliser des captures nommées
  2. Object spread vs. Object.assign
  3. Convertir un objet en Map et réciproquement (cet article)
  4. La boucle for-of : s’il ne devait en rester qu’une…
  5. Simuler une classe abstraite avec new.target
  6. …surprise !

Object, le dictionnaire simple

En JavaScript, pour associer des clés à des valeurs, on a toujours eu ces bons vieux objets (et dès JS 1.1, les littéraux objets) :

// Définition initiale
const classyGuy = {
  first: 'Georges',
  last: 'Abitbol',
  year: 1992,
}

// Lecture statique
classyGuy.first
// Lecture dynamique
classyGuy[formal ? 'last' : 'first']
// Test de présence (dont hérités, ou propres)
'year' in classyGuy
classyGuy.hasOwnProperty('year')
// Ajout
classyGuy.title = 'Homme le plus classe du monde'
// Modification
classyGuy.year = 1993
// Suppression
delete classyGuy.year
// Énumération
for (var prop in classyGuy) {
  console.log(prop, '=', classyGuy[prop])
}

Et au fil des versions (ES3, ES5, ES2015…) on a rajouté plein d’API statiques pour examiner l’objet :

Object.keys(classyGuy)
Object.values(classyGuy)
Object.entries(classyGuy)
Object.getOwnPropertyNames(classyGuy)
// etc.

Super ! Mais alors, à quoi bon Map ?

Map : quels intérêts ?

Recourir à des objets simples pour nos dictionnaires est certes très pratique et concis, mais y’a quand même des limitations nettes :

  • Les clés sont forcément de type String (ou, depuis ES2015, Symbol) : impossible d’utiliser un objet à nous, ou un objet hôte (nœud du DOM, requête fetch, etc.) comme clé.
  • Confusion entre propriétés héritées et propriétés propres (sans parler de leur énumérabilité). En pratique, comme on utilise souvent des objets simples, on n’hérite que de peu de choses depuis Object (toString, valueOf, hasOwnProperty et quelques autres), avec des noms dont le risque de collision sur nos clés est faible. Mais tout de même, pour se blinder, il faudra soit partir d’un Object.create(null) plutôt qu’un {}, soit utiliser systématiquement des API adaptées (telles que Object.getOwnPropertyNames(…), hasOwnProperty(…), etc.).
  • Pas d’API simple de purge. Pas de Object.clear(…) ou équivalent : si on veut préserver l’identité du conteneur tout en le « nettoyant », il faut se galérer un peu.
  • L’ordre d’itération n’est pas garanti. Même si en pratique l’ordre utilisé par forin, Object.keys() et consorts est le plus souvent l’ordre chronologique d’ajout, la spécification ne l’impose pas et des variantes existent.
  • L’itérabilité n’est pas là par défaut. Un objet simple n’est pas itérable (au sens ES2015) par défaut, ce qui empêche de l’utiliser facilement avec le spread, la déstructuration positionnelle, la boucle forof ou tout autre mécanisme de consommation d’un itérable (notamment de multiples API standard).
  • Les performances souffrent en scénario de mutation intense. Pour pouvoir correctement optimiser les indexations de l’objet (l’accès à ses propriétés), qu’elles soient directes ou indirectes, les moteurs JS ont besoin que la « forme » (shape) de ce dernier ne change pas souvent : la plupart du temps, changer la forme purge tous les caches d’optimisation de lookup. Du coup, si on ajoute/supprime beaucoup dans le conteneur, la performance de consultation va souffrir.

C’est pourquoi la bibliothèque standard accueille depuis ES2015 le nouveau type Map. Au prix d’une API un peu plus explicite (et d’un besoin de conversion pour (dé)sérialiser en JSON), on gagne pas mal de choses :

  • Clés de n’importe quel type (même undefined est une clé acceptable)
  • Performance optimale
  • Itérable par défaut
  • API plus riche (dont la purge)
// Définition initiale
const classyGuy = new Map([
  ['first', 'Georges'],
  ['last', 'Abitbol'],
  ['year', 1992],
])

// Lecture
classyGuy.get('first')
// Test de présence
classyGuy.has('year')
// Ajout / modification -- tous types de clés !
classyGuy.set('title', 'Homme le plus classe du monde')
classyGuy.set(classyGuy, 'OMG SO META')
classyGuy.set(null, 'Trop nul')
// Suppression
classyGuy.remove('year')
// Énumération (moult options, je prends la basique)
for (const [key, value] of classyGuy) {
  console.log(key, '=', value)
}
// Purge
classyGuy.clear()

Comment convertir entre les deux ?

Tu peux avoir besoin, de temps en temps, de passer d’une Map à un objet simple. Par exemple, pour le sérialiser en JSON avant de l’envoyer sur le réseau ou de le persister sur disque (et réciproquement : revenir à une Map après récupération réseau ou lecture depuis le disque).

Si ta Map a des clés qui ne sont ni des String ni des Symbol, ça va poser problème, tu t’en doutes. Mais dans le cas contraire, il existe un one-liner pour chaque sens :

// Map -> Object (ES2019+)
Object.fromEntries(map.entries())

// Object -> Map (ES2015)
new Map(Object.entries(obj))

En ES2019, on a enfin Object.fromEntries(…), la réciproque du Object.entries(…) de ES2017 (attendu de longue date lui aussi), qui lui-même nous simplifiait la vie par rapport au Object.keys(…) de ES5 (2009).

En pratique, ça se polyfille très bien (via les canaux habituels : core-js, polyfill.io, etc.), donc même sur IE9+ tu l’aurais (et tu pourrais faire polyfiller Map, aussi).

Si tu veux absolument te limiter à du ES2015 sans polyfiller (mais pourquoi ?! Tu aimes souffrir ?), y’a moyen de le faire en une expression… mais v’là l’expression. Ça implique map.entries(), Array.from, reduce, les descripteurs de propriétés et Object.create. Comme on dit dans la littérature académique, je le « laisse en exercice pour le lecteur ».

C’est dispo où ?!

  • Object.entries(…) est native depuis Chrome 54, Edge 14, Firefox 47, Opera 41, Safari 10.1 et Node 7.
  • Map est native à partir Chrome 38, Edge 12, Firefox 36 (mais 20 pour ce dont on a besoin !), Opera 25, Safari 8 et Node 4.
  • Object.fromEntries(…) est native à partir de Chrome 73, Edge 79, Firefox 63, Opera 60, Safari 12.1 et Node 12.

Encore une fois, ça se polyfille très bien.

Envie d’en savoir plus ?

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 !

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…