2 janvier 2021

Map and Set

Jusqu’à présent, nous avons découvert les structures de données complexes suivantes :

  • Les objets sont utilisés pour stocker des collections de clés.
  • Les tableaux sont utilisés pour stocker des collections ordonnées.

Map

Une Map (dictionnaire de donnée) permet, comme pour un Object, de stocker plusieurs éléments sous la forme de clés valeurs. Sauf que cette fois, les clés peuvent être de n’importe qu’elle type.

Voici les méthodes et les propriétés d’une Map :

  • new Map() – instancie la map.
  • map.set(key, value) – définie la valeur pour une clé.
  • map.get(key) – retourne la valeur associée à la clé, undefined si key n’existe pas dans la map.
  • map.has(key) – retourne true si key existe, sinon false.
  • map.delete(key) – supprime la valeur associée à key
  • map.clear() – supprime tout le contenu de la map.
  • map.size – retourne le nombre d’éléments.

Par exemple :

let map = new Map();

map.set('1', 'str1');   // une clé de type chaîne de caractère
map.set(1, 'num1');     // une clé de type numérique
map.set(true, 'bool1'); // une clé de type booléenne

// souvenez-vous, dans un `Object`, les clés sont converties en chaîne de caractères
// alors que `Map` conserve le type d'origine de la clé,
// c'est pourquoi les deux appels suivants retournent des valeurs différentes:

alert( map.get(1)   ); // 'num1'
alert( map.get('1') ); // 'str1'

alert( map.size ); // 3

Au travers de cet exemple nous pouvons voir, qu’à la différence des Objects, les clés ne sont pas converties en chaîne de caractère. Il est donc possible d’utiliser n’importe quel type.

map[key] n’est pas la bonne façon d’utiliser un Map

Bien que map[key] fonctionne également, par exemple nous pouvons définir map[key] = 2, cela traite map comme un objet JavaScript simple, ce qui implique toutes les limitations correspondantes (uniquement des clés chaîne de caractères/symbol etc…).

Nous devons donc utiliser les méthodes map: set, get et ainsi de suite.

Map peut également utiliser des objets comme clés.

Par exemple:

let john = { name: "John" };

// pour chaque utilisateur, nous stockons le nombre de visites
let visitsCountMap = new Map();

// john est utilisé comme clé dans la map
visitsCountMap.set(john, 123);

alert( visitsCountMap.get(john) ); // 123

Utiliser des objets comme clés est l’une des fonctionnalités les plus notables et les plus importantes de Map. La même chose ne compte pas pour Object. Une chaîne de caractères comme clé dans Object est très bien, mais nous ne pouvons pas utiliser un autre Object comme clé dans Object.

Essayons de faire comme l’exemple précédent directement avec un Object:

let john = { name: "John" };
let ben = { name: "Ben" };

let visitsCountObj = {}; // on créé notre object

visitsCountObj[ben] = 234; // essayez d'utiliser l'objet ben comme clé
visitsCountObj[john] = 123; // essayez d'utiliser l'objet john comme clé, l'objet ben sera remplacé

// C'est ce qui a été écrit!
alert( visitsCountObj["[object Object]"] ); // 123

Comme visitesCountObj est un objet, il convertit toutes les clés Object, telles que john et ben ci-dessus, en la même chaîne de caractères "[object Object]". Certainement pas ce que nous voulons.

Comment Map compare les clés

Pour tester l’égalité entre les clés, Map se base sur l’algorithme SameValueZero. C’est grosso modo la même chose que l’opérateur de stricte égalité ===, à la différence que NaN est considéré comme étant égal à NaN. NaN peut donc être utilisé comme clé.

Cet algorithme ne peut pas peut être modifié.

Chaining

Chaque appel à map.set retourne la map elle-même, ce qui nous permet d’enchaîner les appels:

map.set('1', 'str1')
  .set(1, 'num1')
  .set(true, 'bool1');

Parcourir les éléments d’une Map

Il existe 3 façons de parcourir les éléments d’une map :

  • map.keys() – retourne toutes les clés sous forme d’iterable,
  • map.values() – retourne les valeurs sous forme d’iterable,
  • map.entries() – retourne les entries (couple sous forme de [clé, valeur]), c’est la méthode utilisée par défaut par for..of.

Par exemple :

let recipeMap = new Map([
  ['cucumber', 500],
  ['tomatoes', 350],
  ['onion',    50]
]);

// on parcourt les clés (les légumes)
for (let vegetable of recipeMap.keys()) {
  alert(vegetable); // cucumber, tomatoes, onion
}

// on parcourt les valeurs (les montants)
for (let amount of recipeMap.values()) {
  alert(amount); // 500, 350, 50
}

// on parcourt les entries (couple [clé, valeur])
for (let entry of recipeMap) { // équivalent à : recipeMap.entries()
  alert(entry); // cucumber,500 (etc...)
}
L’ordre d’insertion est conservé

Contraitement aux Object, Map conserve l’ordre d’insertion des valeurs.

Il est aussi possible d’utiliser forEach avec Map comme on pourrait le faire avec un tableau :

// exécute la fonction pour chaque couple (key, value)
recipeMap.forEach( (value, key, map) => {
  alert(`${key}: ${value}`); // cucumber: 500 etc
});

Object.entries: Map from Object

When a Map is created, we can pass an array (or another iterable) with key/value pairs for initialization, like this:

// array of [key, value] pairs
let map = new Map([
  ['1',  'str1'],
  [1,    'num1'],
  [true, 'bool1']
]);

alert( map.get('1') ); // str1

If we have a plain object, and we’d like to create a Map from it, then we can use built-in method Object.entries(obj) that returns an array of key/value pairs for an object exactly in that format.

So we can create a map from an object like this:

let obj = {
  name: "John",
  age: 30
};

let map = new Map(Object.entries(obj));

alert( map.get('name') ); // John

Here, Object.entries returns the array of key/value pairs: [ ["name","John"], ["age", 30] ]. That’s what Map needs.

Object.fromEntries: Object from Map

We’ve just seen how to create Map from a plain object with Object.entries(obj).

There’s Object.fromEntries method that does the reverse: given an array of [key, value] pairs, it creates an object from them:

let prices = Object.fromEntries([
  ['banana', 1],
  ['orange', 2],
  ['meat', 4]
]);

// now prices = { banana: 1, orange: 2, meat: 4 }

alert(prices.orange); // 2

We can use Object.fromEntries to get a plain object from Map.

E.g. we store the data in a Map, but we need to pass it to a 3rd-party code that expects a plain object.

Here we go:

let map = new Map();
map.set('banana', 1);
map.set('orange', 2);
map.set('meat', 4);

let obj = Object.fromEntries(map.entries()); // make a plain object (*)

// done!
// obj = { banana: 1, orange: 2, meat: 4 }

alert(obj.orange); // 2

A call to map.entries() returns an iterable of key/value pairs, exactly in the right format for Object.fromEntries.

We could also make line (*) shorter:

let obj = Object.fromEntries(map); // omit .entries()

That’s the same, because Object.fromEntries expects an iterable object as the argument. Not necessarily an array. And the standard iteration for map returns same key/value pairs as map.entries(). So we get a plain object with same key/values as the map.

Set

Set est une liste sans doublons.

Ses principales méthodes sont :

  • new Set(iterable) – créé un set, si un iterable (la plupart du temps, un tableau) est passé en paramètre, ses valeurs sont copiées dans le set
  • set.add(value) – ajoute l’élément value et retourne le set.
  • set.delete(value) – supprime l’élément value et retourne true si la valeur existait au moment de l’appel sinon false.
  • set.has(value) – retourne true si la valeur existe dans le set, sinon faux.
  • set.clear() – supprime tout le contenu du set.
  • set.size – le nombre d’éléments dans le tableau.

Ce qu’il faut surtout savoir c’est que lorsque l’on appelle plusieurs fois set.add(value) avec la même valeur, la méthode ne fait rien. C’est pourquoi chaque valeur est unique dans un Set.

Par exemple, nous souhaitons nous souvenir de tous nos visiteurs. Mais chaque visiteurs doit être unique.

Set est exactement ce qu’il nous faut :

let set = new Set();

let john = { name: "John" };
let pete = { name: "Pete" };
let mary = { name: "Mary" };

// visites, certains utilisateurs viennent plusieurs fois
set.add(john);
set.add(pete);
set.add(mary);
set.add(john);
set.add(mary);

// set conserve une fois chaque visiteurs
alert( set.size ); // 3

for (let user of set) {
  alert(user.name); // John (puis Pete et Mary)
}

Nous aurions aussi pu utiliser un tableau (Array) en vérifiant avant chaque insertion que l’élément n’existe pas en utilisant arr.find. Cependant les performances auraient été moins bonnes car cette méthode parcours chaque élément du tableau. Set est beaucoup plus efficace car il est optimisé en interne pour vérifier l’unicité des valeurs.

Parcourir un Set

Nous pouvons parcourir les éléments d’un Set avec for..of ou en utilisant forEach

let set = new Set(["oranges", "apples", "bananas"]);

for (let value of set) alert(value);

// même chose en utilisant forEach:
set.forEach((value, valueAgain, set) => {
  alert(value);
});

A noter que la fonction de callback utilisée par forEach prend 3 arguments en paramètres: une value, puis la même valeur valueAgain, et enfin le set lui-même.

C’est pour des raisons de compatibilité avec Map que forEach prend en paramètre 3 arguments. C’est quelque peu surprenant, mais cela permet de remplacer facilement une Map par un Set.

Les méthodes pour parcourir les éléments d’une Map peuvent être utilisées :

  • set.keys() – retourne un objet iterable contenant les valeurs,
  • set.values() – même chose que pour set.keys(), méthode présente pour des raisons de compatibilité avec Map,
  • set.entries() – retourne un objet iterable sous la forme de [value, value], , méthode présente pour des raisons de compatibilité avec Map

Summary

Map – est une collection de clé valeurs.

Méthodes et propriétés:

  • new Map([iterable]) – créé une map, potentiellement initialisée avec un iterable (ex: un array) de couple clé valeur [key, value].
  • map.set(key, value) – stocke la valeur par la clé, renvoie la carte elle-même.
  • map.get(key) – retourne la valeur associée à key, undefined si key n’existe pas.
  • map.has(key) – retourne true si key existe sinon false.
  • map.delete(key) – supprime la valeur par la clé, retourne true si key existait au moment de l’appel, sinon false.
  • map.clear() – supprime tous les éléments dans la map.
  • map.size – retourne le nombre d’éléments.

La différence entre Map avec un objet traditionel :

  • N’importe quel type peut être utilisé comme clé
  • Accès à des méthodes tels que size.

Set – est une collection de valeurs uniques

Méthodes et propriétés :

  • new Set([iterable]) – créé un set, potentiellement initialisé avec un iterable (ex: un array) de valeurs.
  • set.add(value) – ajoute une valeur sauf si elle existe et retourne le set en cours.
  • set.delete(value) – supprime la valeur, retourne true si value existait au moment de l’appel sinon false.
  • set.has(value) – retourne true si la valeur existe dans le set, sinon false.
  • set.clear() – supprime tous les éléments du set.
  • set.size – retourne le nombre d’éléments.

On ne peut pas dire que les éléments dans une Map ou un Set sont désordonnés car ils sont toujours parcourut par ordre d’insertion. Il est cependant impossible de réorganiser les éléments ou bien de les retrouver par leur index.

Exercices

importance: 5

Disons que arr est un tableau.

Créez une fonction unique(arr) qui devrait renvoyer un tableau avec les éléments uniques d’arr.

Par exemple :

function unique(arr) {
  /* your code */
}

let values = ["Hare", "Krishna", "Hare", "Krishna",
  "Krishna", "Krishna", "Hare", "Hare", ":-O"
];

alert( unique(values) ); // Hare, Krishna, :-O

P.S. Ici, les chaînes de caractères sont utilisées, mais elles peuvent être des valeurs de n’importe quel type.

P.P.S. Utilisez Set pour stocker des valeurs uniques.

Open a sandbox with tests.

function unique(arr) {
  return Array.from(new Set(arr));
}

Ouvrez la solution avec des tests dans une sandbox.

importance: 4

Anagrams sont des mots qui ont le même nombre de mêmes lettres, mais dans un ordre différent.

Par exemple :

nap - pan
ear - are - era
cheaters - hectares - teachers

Ecrivez une fonction aclean(arr) qui retourne un tableau nettoyé des anagrammes.

Par exemple :

let arr = ["nap", "teachers", "cheaters", "PAN", "ear", "era", "hectares"];

alert( aclean(arr) ); // "nap,teachers,ear" ou "PAN,cheaters,era"

De chaque groupe d’anagrammes ne devrait rester qu’un mot, peu importe lequel.

Open a sandbox with tests.

Pour trouver tous les anagrammes, divisons chaque mot en lettres et trions-les. Lorsque ils sont triés par lettres, tous les anagrammes sont identiques.

Par exemple :

nap, pan -> anp
ear, era, are -> aer
cheaters, hectares, teachers -> aceehrst
...

Nous allons utiliser les variantes triées par lettre comme clés de map pour stocker une seule valeur pour chaque clé :

function aclean(arr) {
  let map = new Map();

  for (let word of arr) {
    // diviser le mot en lettres, les trier et les rejoindre
    let sorted = word.toLowerCase().split('').sort().join(''); // (*)
    map.set(sorted, word);
  }

  return Array.from(map.values());
}

let arr = ["nap", "teachers", "cheaters", "PAN", "ear", "era", "hectares"];

alert( aclean(arr) );

Le tri des lettres se fait par la chaîne d’appels en ligne (*).

Pour plus de commodité, divisons-le en plusieurs lignes :

let sorted = word // PAN
  .toLowerCase() // pan
  .split('') // ['p','a','n']
  .sort() // ['a','n','p']
  .join(''); // anp

Deux mots différents, 'PAN' et 'nap', reçoivent la même forme de lettre triée 'anp'.

La ligne suivante place le mot dans le map :

map.set(sorted, word);

Si nous rencontrons à nouveau un mot trié de la même manière, il écrasera la valeur précédente avec la même clé dans le map. Nous aurons donc toujours au maximum un mot par lettre.

À la fin Array.from(map.values()) prends un itérable sur les valeurs du Map (nous n’avons pas besoin des clés dans le résultat) et renvoi un tableau avec celles-ci.

Ici, nous pourrions également utiliser un objet simple au lieu du Map, car les clés sont des chaînes de caractères.

Voilà à quoi la solution peut ressembler :

function aclean(arr) {
  let obj = {};

  for (let i = 0; i < arr.length; i++) {
    let sorted = arr[i].toLowerCase().split("").sort().join("");
    obj[sorted] = arr[i];
  }

  return Object.values(obj);
}

let arr = ["nap", "teachers", "cheaters", "PAN", "ear", "era", "hectares"];

alert( aclean(arr) );

Ouvrez la solution avec des tests dans une sandbox.

importance: 5

Nous voulons obtenir un tableau de map.keys() dans une variable puis lui appliquer des méthodes spécifiques aux tableaux, par ex: .push.

Mais cela ne fonctionne pas :

let map = new Map();

map.set("name", "John");

let keys = map.keys();

// Error: keys.push is not a function
keys.push("more");

Pourquoi ? Comment pouvons-nous corriger le code pour que keys.push fonctionne ?

C’est parce que map.keys() retourne un itérable, mais pas un tableau.

Nous pouvons le convertir en tableau en utilisant Array.from :

let map = new Map();

map.set("name", "John");

let keys = Array.from(map.keys());

keys.push("more");

alert(keys); // name, more
Carte du tutoriel

Commentaires

lire ceci avant de commenter…
  • Si vous avez des améliorations à suggérer, merci de soumettre une issue GitHub ou une pull request au lieu de commenter.
  • Si vous ne comprenez pas quelque chose dans l'article, merci de préciser.
  • Pour insérer quelques bouts de code, utilisez la balise <code>, pour plusieurs lignes – enveloppez-les avec la balise <pre>, pour plus de 10 lignes - utilisez une sandbox (plnkr, jsbin, codepen…)