🌐 AIæœçŽą & 代理 䞻饔
19 octobre 2023

WeakMap et WeakSet

Comme nous le savons du chapitre Ramasse-miettes (garbage collection), le moteur JavaScript stocke une valeur en mĂ©moire pendant qu’elle est accessible et peut potentiellement ĂȘtre utilisĂ©e.

Par exemple :

let john = { name: "John" };

// l'objet est accessible, john en est la référence

// écraser la référence
john = null;

// l'objet sera supprimé de la mémoire

Habituellement, les propriĂ©tĂ©s d’un objet ou des Ă©lĂ©ments d’un tableau ou d’une autre structure de donnĂ©es sont considĂ©rĂ©es comme accessibles et conservĂ©es en mĂ©moire pendant que cette structure de donnĂ©es est en mĂ©moire.

Par exemple, si nous mettons un objet dans un tableau, alors que le tableau est vivant, l’objet sera Ă©galement vivant, mĂȘme s’il n’y a pas d’autres rĂ©fĂ©rences.

Comme ceci :

let john = { name: "John" };

let array = [ john ];

john = null; // écraser la référence

// l'objet précédemment référencé par john est stocké dans le tableau
// donc il ne sera pas nettoyé
// nous pouvons l'obtenir sous forme de array[0]

Semblable Ă  cela, si nous utilisons un objet comme clĂ© dans un Map classique, alors que le Map existe, cet objet existe Ă©galement. Il occupe de la mĂ©moire et ne peut pas ĂȘtre nettoyĂ© (garbage collected).

Par example :

let john = { name: "John" };

let map = new Map();
map.set(john, "...");

john = null; // écraser la référence

// John est stocké à l'intérieur du map
// nous pouvons l'obtenir en utilisant map.keys()

WeakMap est fondamentalement diffĂ©rent Ă  cet Ă©gard. Cela n’empĂȘche pas le garbage collection des objets clĂ©s.

Voyons ce que cela signifie sur des exemples.

WeakMap

La premiĂšre diffĂ©rences entre Map et WeakMap est que les clĂ©s doivent ĂȘtre des objets, pas des valeurs primitives :

let weakMap = new WeakMap();

let obj = {};

weakMap.set(obj, "ok"); // fonctionne bien (object key)

// ne peut pas utiliser une chaßne de caractÚres comme clé
weakMap.set("test", "Whoops"); // Erreur, parce que "test" n'est pas un objet

Maintenant, si nous utilisons un objet comme clĂ©, et qu’il n’y a pas d’autres rĂ©fĂ©rences Ă  cet objet – il sera automatiquement supprimĂ© de la mĂ©moire (et du map).

let john = { name: "John" };

let weakMap = new WeakMap();
weakMap.set(john, "...");

john = null; // on écrase la référence

// John est supprimé de la mémoire !

Comparez-le avec l’exemple du Map ci-dessus. Maintenant, si john n’existe que comme clĂ© de WeakMap – il sera automatiquement supprimĂ© du map (et de la mĂ©moire).

WeakMap ne prend pas en charge l’itĂ©ration et les mĂ©thodes keys(), values(), entries(), il n’y a donc aucun moyen d’en obtenir toutes les clĂ©s ou valeurs.

WeakMap n’a que les mĂ©thodes suivantes :

Pourquoi une telle limitation ? C’est pour des raisons techniques. Si un objet a perdu toutes les autres rĂ©fĂ©rences (comme john dans le code ci-dessus), il doit ĂȘtre automatiquement nettoyĂ©. Mais techniquement, ce n’est pas exactement spĂ©cifiĂ© quand le nettoyage a lieu.

Le moteur JavaScript dĂ©cide de cela. Il peut choisir d’effectuer le nettoyage de la mĂ©moire immĂ©diatement ou d’attendre et de faire le nettoyage plus tard lorsque d’autres suppressions se produisent. Donc, techniquement, le nombre d’élĂ©ments actuel d’un WeakMap n’est pas connu. Le moteur peut l’avoir nettoyĂ© ou non, ou l’a fait partiellement. Pour cette raison, les mĂ©thodes qui accĂšdent Ă  toutes les clĂ©s/valeurs ne sont pas prises en charge.

Maintenant, oĂč avons-nous besoin d’une telle structure de donnĂ©es ?

Cas d’utilisation : donnĂ©es supplĂ©mentaires

Le principal domaine d’application de WeakMap est un stockage de donnĂ©es supplĂ©mentaire.

Si nous travaillons avec un objet qui “appartient” Ă  un autre code, peut-ĂȘtre mĂȘme une bibliothĂšque tierce, et que nous souhaitons stocker certaines donnĂ©es qui lui sont associĂ©es, cela ne devrait exister que lorsque l’objet est vivant – alors WeakMap est exactement ce qu’il nous faut.

Nous plaçons les donnĂ©es dans un WeakMap, en utilisant l’objet comme clĂ©, et lorsque l’objet est nettoyĂ©, ces donnĂ©es disparaissent automatiquement Ă©galement.

weakMap.set(john, "secret documents");
// si John meurt, les documents secrets seront détruits automatiquement

Regardons un exemple.

Par exemple, nous avons un code qui conserve un nombre de visites pour les utilisateurs. Les informations sont stockĂ©es dans un map : un objet utilisateur est la clĂ© et le nombre de visites est la valeur. Lorsqu’un utilisateur quitte (son objet est nettoyĂ©), nous ne voulons plus stocker son nombre de visites.

Voici un exemple d’une fonction de comptage avec Map :

// 📁 visitsCount.js
let visitsCountMap = new Map(); // map: user => visits count

// augmentons le nombre de visites
function countUser(user) {
  let count = visitsCountMap.get(user) || 0;
  visitsCountMap.set(user, count + 1);
}

Et voici une autre partie du code, peut-ĂȘtre un autre fichier qui l’utilise :

// 📁 main.js
let john = { name: "John" };

countUser(john); // compter ses visites

// plus tard, John nous quitte
john = null;

Maintenant, l’objet john doit ĂȘtre nettoyĂ©, mais cependant, il reste en mĂ©moire, parce que c’est une clĂ© dans visitesCountMap.

Nous devons nettoyer visitesCountMap lorsque nous supprimons des utilisateurs, sinon il augmentera indéfiniment en mémoire. Un tel nettoyage peut devenir une tùche fastidieuse dans des architectures complexes.

Nous pouvons éviter cela en utilisant WeakMap :

// 📁 visitsCount.js
let visitsCountMap = new WeakMap(); // weakmap: user => visits count

// augmentons le nombre de visites
function countUser(user) {
  let count = visitsCountMap.get(user) || 0;
  visitsCountMap.set(user, count + 1);
}

Maintenant, nous n’avons plus Ă  nettoyer visitesCountMap. AprĂšs que l’objet john devienne inaccessible autrement que en tant que clĂ© de WeakMap, il est supprimĂ© de la mĂ©moire, en mĂȘme temps que les informations de cette clĂ© dans WeakMap.

Cas d’utilisation : mise en cache

Un autre exemple courant est la mise en cache. Nous pouvons stocker (“cache”) les rĂ©sultats d’une fonction, afin que les futurs appels sur le mĂȘme objet puissent le rĂ©utiliser.

Pour y parvenir, nous pouvons utiliser Map (scénario non optimal) :

// 📁 cache.js
let cache = new Map();

// calculons et mémorisons le résultat
function process(obj) {
  if (!cache.has(obj)) {
    let result = /* calculs du résultat pour */ obj;

    cache.set(obj, result);
    return result;
  }

  return cache.get(obj);
}

// Maintenant, utilisons process() dans un autre fichier :

// 📁 main.js
let obj = {/* disons que nous avons un objet */};

let result1 = process(obj); // calculé

// 
 plus tard, d'un autre endroit du code 

let result2 = process(obj); // résultat mémorisé provenant du cache

// 
 plus tard, lorsque l'objet n'est plus nécessaire :
obj = null;

alert(cache.size); // 1 (Ouch ! L'objet est toujours dans le cache, prenant de la mémoire !)

Pour plusieurs appels de process(obj) avec le mĂȘme objet, il ne calcule le rĂ©sultat que la premiĂšre fois, puis le prend simplement dans cache. L’inconvĂ©nient est que nous devons nettoyer le cache lorsque l’objet n’est plus nĂ©cessaire.

Si nous remplaçons Map par WeakMap, alors ce problĂšme disparaĂźt : le rĂ©sultat mis en cache sera automatiquement supprimĂ© de la mĂ©moire une fois que l’objet sera nettoyĂ©.

// 📁 cache.js
let cache = new WeakMap();

// calculons et mémorisons le résultat
function process(obj) {
  if (!cache.has(obj)) {
    let result = /* calculer le résultat pour */ obj;

    cache.set(obj, result);
    return result;
  }

  return cache.get(obj);
}

// 📁 main.js
let obj = {/* un objet */};

let result1 = process(obj);
let result2 = process(obj);

// 
 plus tard, lorsque l'objet n'est plus nécessaire :
obj = null;

// Impossible d'obtenir cache.size, car c'est un WeakMap,
// mais c'est 0 ou bientĂŽt 0
// Lorsque obj est nettoyé, les données mises en cache seront également supprimées

WeakSet

WeakSet se comporte de la mĂȘme maniĂšre :

  • Il est analogue Ă  Set, mais nous pouvons seulement ajouter des objets Ă  WeakSet (pas de primitives).
  • Un objet existe dans le set tant qu’il est accessible ailleurs.
  • Comme Set, il prend en charge add, has et delete, mais pas size, keys() et aucune itĂ©ration.

Étant “weak” (faible), il sert Ă©galement de stockage supplĂ©mentaire. Mais pas pour des donnĂ©es arbitraires, mais plutĂŽt pour des faits “oui/non”. Une appartenance Ă  WeakSet peut signifier quelque chose Ă  propos de l’objet.

Par exemple, nous pouvons ajouter des utilisateurs à WeakSet pour garder une trace de ceux qui ont visité notre site :

let visitedSet = new WeakSet();

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

visitedSet.add(john); // John nous a rendu visite
visitedSet.add(pete); // Ensuite Pete
visitedSet.add(john); // John encore

// visitedSet a 2 utilisateurs maintenant

// vérifions si John est venu
alert(visitedSet.has(john)); // true

// vérifions si Mary est venue
alert(visitedSet.has(mary)); // false

john = null;

// visitedSet sera nettoyé automatiquement

La limitation la plus notable de WeakMap et WeakSet est l’absence d’itĂ©rations et l’impossibilitĂ© d’obtenir tout le contenu actuel. Cela peut sembler gĂȘnant, mais n’empĂȘche pas WeakMap/WeakSet de faire leur travail principal – ĂȘtre un stockage “supplĂ©mentaire” de donnĂ©es pour les objets qui sont stockĂ©s/gĂ©rĂ©s Ă  un autre endroit.

Résumé

WeakMap est une sorte de collection Map qui n’autorise que des objets comme clĂ©s et les supprime avec la valeur associĂ©e une fois qu’ils deviennent inaccessibles par d’autres moyens.

WeakSet est une sorte de collection Set qui ne stocke que des objets et les supprime une fois qu’ils deviennent inaccessibles par d’autres moyens.

Leurs principaux avantages sont qu’ils ont une faible rĂ©fĂ©rence aux objets, de sorte qu’ils peuvent facilement ĂȘtre supprimĂ©s par le garbage collector.

Cela se fait au prix de ne pas avoir de support pour clear, size, keys, values


WeakMap et WeakSet sont utilisĂ©es comme structures de donnĂ©es “secondaires” en plus du stockage d’objets “principal”. Une fois que l’objet est retirĂ© du stockage principal, s’il n’est trouvĂ© que comme clĂ© de WeakMap ou dans un WeakSet, il sera nettoyĂ© automatiquement.

Exercices

importance: 5

Il y a un tableau de messages :

let messages = [
  {text: "Hello", from: "John"},
  {text: "How goes?", from: "John"},
  {text: "See you soon", from: "Alice"}
];

Votre code peut y accĂ©der, mais les messages sont gĂ©rĂ©s par le code d’une autre personne. De nouveaux messages sont ajoutĂ©s, les anciens sont rĂ©guliĂšrement supprimĂ©s par ce code et vous ne connaissez pas le moment exact oĂč cela se produit.

Maintenant, quelle structure de donnĂ©es pouvez-vous utiliser pour stocker des informations si le message “a Ă©tĂ© lu” ? La structure doit ĂȘtre bien adaptĂ©e pour donner la rĂ©ponse “a-t-il Ă©tĂ© lu ?” Pour l’objet de message donnĂ©.

P.S. Lorsqu’un message est supprimĂ© des messages, il doit Ă©galement disparaĂźtre de votre structure.

P.P.S. Nous ne devrions pas modifier les objets de message, leur ajouter nos propriĂ©tĂ©s. Comme ils sont gĂ©rĂ©s par le code de quelqu’un d’autre, cela peut avoir de mauvaises consĂ©quences.

Stockons les messages lus dans WeakSet:

let messages = [
  {text: "Hello", from: "John"},
  {text: "How goes?", from: "John"},
  {text: "See you soon", from: "Alice"}
];

let readMessages = new WeakSet();

// deux messages ont été lus
readMessages.add(messages[0]);
readMessages.add(messages[1]);
// readMessages a 2 éléments

// ...Relisons le premier message !
readMessages.add(messages[0]);
// readMessages a encore 2 éléments uniques

// réponse : le message[0] a-t-il été lu ?
alert("Read message 0: " + readMessages.has(messages[0])); // true

messages.shift();
// maintenant readMessages a 1 Ă©lĂ©ment (techniquement, la mĂ©moire peut ĂȘtre nettoyĂ©e plus tard)

Le WeakSet permet de stocker un ensemble de messages et de vĂ©rifier facilement l’existence d’un message dedans.

Il se nettoie automatiquement. Le compromis est que nous ne pouvons pas le parcourir, nous ne pouvons pas obtenir “tous les messages lus” directement. Mais nous pouvons le faire en parcourant tous les messages et en filtrant ceux qui sont dans le set.

Une autre solution pourrait consister à ajouter une propriété telle que message.isRead = true à un message aprÚs sa lecture. Comme les objets de messages sont gérés par un autre code, cela est généralement déconseillé, mais nous pouvons utiliser une propriété symbolique pour éviter les conflits.

Comme ceci :

// la propriété symbolique n'est connue que de notre code
let isRead = Symbol("isRead");
messages[0][isRead] = true;

Maintenant, le code tiers ne verra probablement pas notre propriété supplémentaire.

Bien que les symboles permettent de rĂ©duire la probabilitĂ© de problĂšmes, l’utilisation de WeakSet est prĂ©fĂ©rable du point de vue de l’architecture.

importance: 5

Il existe un tableau de messages comme dans la previous task. La situation est similaire.

let messages = [
  {text: "Hello", from: "John"},
  {text: "How goes?", from: "John"},
  {text: "See you soon", from: "Alice"}
];

La question qui se pose maintenant est la suivante : quelle structure de donnĂ©es suggĂ©rez-vous pour stocker les informations : “quand le message a-t-il Ă©tĂ© lu ?”.

Dans la tĂąche prĂ©cĂ©dente, nous n’avions besoin que de stocker le fait “oui/non”. Nous devons maintenant stocker la date et elle ne doit rester en mĂ©moire que tant que le message n’a pas Ă©tĂ© nettoyĂ©.

P.S. Les dates peuvent ĂȘtre stockĂ©es en tant qu’objets de la classe intĂ©grĂ©e Date, que nous couvrirons plus tard.

Pour stocker une date, nous pouvons utiliser WeakMap:

let messages = [
  {text: "Hello", from: "John"},
  {text: "How goes?", from: "John"},
  {text: "See you soon", from: "Alice"}
];

let readMap = new WeakMap();

readMap.set(messages[0], new Date(2017, 1, 1));
// objet Date que nous étudierons plus tard
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
)