web-dev-qa-db-fra.com

Comment conserver une carte ES6 dans localstorage (ou ailleurs)?

var a = new Map([[ 'a', 1 ]]);
a.get('a') // 1

var forStorageSomewhere = JSON.stringify(a);
// Store, in my case, in localStorage.

// Later:
var a = JSON.parse(forStorageSomewhere);
a.get('a') // TypeError: undefined is not a function

Malheureusement, JSON.stringify(a); renvoie simplement '{}', ce qui signifie qu'un devient un objet vide lorsqu'il est restauré.

J'ai trouvé es6-mapify qui permet une conversion ascendante/descendante entre une carte et un objet simple, ce qui pourrait être une solution, mais j'espérais avoir besoin de recourir à une dépendance externe simplement pour persister ma carte.

60
Letharion

En supposant que vos clés et vos valeurs soient serialisables,

localStorage.myMap = JSON.stringify(Array.from(map.entries()));

devrait marcher. Pour l'inverse, utilisez

map = new Map(JSON.parse(localStorage.myMap));
70
Bergi

Habituellement, la sérialisation n'est utile que si cette propriété contient

deserialize(serialize(data)).get(key) ≈ data.get(key)

a ≈ b pourrait être défini comme serialize(a) === serialize(b).

Ceci est satisfait lors de la sérialisation d'un objet en JSON:

var obj1 = {foo: [1,2]},
    obj2 = JSON.parse(JSON.stringify(obj1));
obj1.foo; // [1,2]
obj2.foo; // [1,2] :)
JSON.stringify(obj1.foo) === JSON.stringify(obj2.foo); // true :)

Et cela fonctionne parce que les propriétés ne peuvent être que des chaînes, qui peuvent être sérialisées sans perte en chaînes.

Cependant, les cartes ES6 autorisent des valeurs arbitraires comme clés. Cela pose problème car les objets sont identifiés de manière unique par leur référence, et non par leurs données. Et lors de la sérialisation des objets, vous perdez les références.

var key = {},
    map1 = new Map([ [1,2], [key,3] ]),
    map2 = new Map(JSON.parse(JSON.stringify([...map1.entries()])));
map1.get(1); // 2
map2.get(1); // 2 :)
map1.get(key); // 3
map2.get(key); // undefined :(

Je dirais donc en général ce n'est pas possible pour le faire de manière utile.

Et pour les cas où cela fonctionnerait, vous pouvez probablement tilisez un objet simple au lieu d'une carte. Cela aura également ces avantages:

  • Il pourra être stratifié en JSON sans perdre les informations clés.
  • Cela fonctionnera sur les anciens navigateurs.
  • Cela pourrait être plus rapide.
10
Oriol

En partant de Oriol'sanswer , nous pouvons faire un peu mieux. Nous pouvons toujours utiliser des références d'objet pour les clés tant qu'il y a une racine primitive ou une entrée dans la carte, et chaque clé d'objet peut être trouvée de manière transitoire à partir de cette clé racine.

En modifiant l'exemple d'Oriol pour utiliser celui de Douglas Crockford JSON.decycle et JSON.retrocycle nous pouvons créer une carte qui gère ce cas:

var key = {},
    map1 = new Map([ [1, key], [key, 3] ]),
    map2 = new Map(JSON.parse(JSON.stringify([...map1.entries()]))),
    map3 = new Map(JSON.retrocycle(JSON.parse(JSON.stringify(JSON.decycle([...map1.entries()])))));
map1.get(1); // key
map2.get(1); // key
map3.get(1); // key
map1.get(map1.get(1)); // 3 :)
map2.get(map2.get(1)); // undefined :(
map3.get(map3.get(1)); // 3 :)

Le décycle et le rétrocycle permettent de coder des structures cycliques et des dags en JSON. Ceci est utile si nous voulons établir des relations entre les objets sans créer de propriétés supplémentaires sur ces objets eux-mêmes, ou si nous voulons relier de manière interchangeable les primitives aux objets et vice-versa, en utilisant une carte ES6.

Le seul écueil est que nous ne pouvons pas utiliser l'objet clé d'origine pour la nouvelle carte (map3.get(key); renverrait undefined). Cependant, en conservant la référence de clé d'origine, mais une carte JSON nouvellement analysée semble être un cas très improbable.

5
Jonathan Gawrych

Nettoyer comme un sifflet:

JSON.stringify([...myMap])
5
Oded Breiner

La réponse acceptée échouera lorsque vous disposerez de cartes multidimensionnelles. Il faut toujours garder à l'esprit qu'un objet Map peut prendre un autre objet Map comme clé ou valeur.

Ainsi, une façon meilleure et plus sûre de gérer ce travail pourrait être la suivante;

function arrayifyMap(m){
  return m.constructor === Map ? [...m].map(([v,k]) => [arrayifyMap(v),arrayifyMap(k)])
                               : m;
}

Une fois que vous avez cet outil, vous pouvez toujours faire comme;

localStorage.myMap = JSON.stringify(arrayifyMap(myMap))
2
Redu

Si vous implémentez votre propre fonction toJSON() pour tous les objets class que vous avez alors juste les anciens JSON.stringify() fonctionnera juste!

Maps avec Arrays pour les clés? Maps avec d'autres Map comme valeurs? Un Map à l'intérieur d'un Object régulier? Peut-être même votre propre classe personnalisée; facile.

Map.prototype.toJSON = function() {
    return Array.from(this.entries());
};

C'est ça! la manipulation du prototype est requise ici. Vous pouvez contourner l'ajout de toJSON() manuellement à tous vos trucs non standard, mais vous évitez vraiment la puissance de JS

DÉMO

test = {
    regular : 'object',
    map     : new Map([
        [['array', 'key'], 7],
        ['stringKey'     , new Map([
            ['innerMap'    , 'supported'],
            ['anotherValue', 8]
        ])]
    ])
};
console.log(JSON.stringify(test));

les sorties:

{"regular":"object","map":[[["array","key"],7],["stringKey",[["innerMap","supported"],["anotherValue",8]]]]}

La désérialisation depuis le vrai Maps n'est cependant pas aussi automatique. En utilisant la chaîne résultante ci-dessus, je vais refaire les cartes pour extraire une valeur:

test2 = JSON.parse(JSON.stringify(test));
console.log((new Map((new Map(test2.map)).get('stringKey'))).get('innerMap'));

les sorties

"supported"

C'est un peu compliqué, mais avec un peu sauce magique vous pouvez aussi rendre la désérialisation automagique .

Map.prototype.toJSON = function() {
    return ['window.Map', Array.from(this.entries())];
};
Map.fromJSON = function(key, value) {
    return (value instanceof Array && value[0] == 'window.Map') ?
        new Map(value[1]) :
        value
    ;
};

Maintenant, le JSON est

{"regular":"object","test":["window.Map",[[["array","key"],7],["stringKey",["window.Map",[["innerMap","supported"],["anotherValue",8]]]]]]}

Et la désérialisation et l'utilisation sont très simples avec notre Map.fromJSON

test2 = JSON.parse(JSON.stringify(test), Map.fromJSON);
console.log(test2.map.get('stringKey').get('innerMap'));

sorties (et pas de new Map() s utilisées)

"supported"

DÉMO

1
Hashbrown
// store
const mapObj = new Map([['a', 1]]);
localStorage.a = JSON.stringify(mapObj, replacer);

// retrieve
const newMapObj = JSON.parse(localStorage.a, reviver);

// required replacer and reviver functions
function replacer(key, value) {
  const originalObject = this[key];
  if(originalObject instanceof Map) {
    return {
      dataType: 'Map',
      value: Array.from(originalObject.entries()), // or with spread: value: [...originalObject]
    };
  } else {
    return value;
  }
}
function reviver(key, value) {
  if(typeof value === 'object' && value !== null) {
    if (value.dataType === 'Map') {
      return new Map(value.value);
    }
  }
  return value;
}

J'ai écrit ici l'explication sur les fonctions de remplacement et de réactivation ici https://stackoverflow.com/a/56150320/696535

Ce code fonctionnera pour toute autre valeur comme JSON.stringify normal, il n'y a donc pas d'hypothèse que l'objet sérialisé doit être une carte. Il peut également s'agir d'une carte profondément imbriquée dans un tableau ou un objet.

0
Pawel

Une chose qui est laissée de côté est que Map est une structure [~ # ~] ordonnée [~ # ~] - c'est-à-dire que lors de l'itération du premier élément entré, être le premier inscrit.

C'est [~ # ~] pas [~ # ~] comme un objet Javascript. J'ai eu besoin de ce type de structure (j'ai donc utilisé Map) et puis découvrir que JSON.stringify ne fonctionne pas est douloureux (mais compréhensible).

J'ai fini par créer une fonction "value_to_json", ce qui signifie analyser TOUT - en utilisant JSON.stringify uniquement pour les "types" les plus élémentaires.

Malheureusement, sous-classer MAP avec un .toJSON () ne fonctionne pas car il exclut une valeur et non une chaîne JSON. Il est également considéré comme un héritage.

Mon cas d'utilisation serait cependant exceptionnel.

en relation:

function value_to_json(value) {
  if (value === null) {
    return 'null';
  }
  if (value === undefined) {
    return 'null';
  }
  //DEAL WITH +/- INF at your leisure - null instead..

  const type = typeof value;
  //handle as much as possible taht have no side effects. function could
  //return some MAP / SET -> TODO, but not likely
  if (['string', 'boolean', 'number', 'function'].includes(type)) {
    return JSON.stringify(value)
  } else if (Object.prototype.toString.call(value) === '[object Object]') {
    let parts = [];
    for (let key in value) {
      if (Object.prototype.hasOwnProperty.call(value, key)) {
        parts.Push(JSON.stringify(key) + ': ' + value_to_json(value[key]));
      }
    }
    return '{' + parts.join(',') + '}';
  }
  else if (value instanceof Map) {
    let parts_in_order = [];
    value.forEach((entry, key) => {
      if (typeof key === 'string') {
        parts_in_order.Push(JSON.stringify(key) + ':' + value_to_json(entry));
      } else {
        console.log('Non String KEYS in MAP not directly supported');
      }
      //FOR OTHER KEY TYPES ADD CUSTOM... 'Key' encoding...
    });
    return '{' + parts_in_order.join(',') + '}';
  } else if (typeof value[Symbol.iterator] !== "undefined") {
    //Other iterables like SET (also in ORDER)
    let parts = [];
    for (let entry of value) {
      parts.Push(value_to_json(entry))
    }
    return '[' + parts.join(',') + ']';
  } else {
    return JSON.stringify(value)
  }
}


let m = new Map();
m.set('first', 'first_value');
m.set('second', 'second_value');
let m2 = new Map();
m2.set('nested', 'nested_value');
m.set('sub_map', m2);
let map_in_array = new Map();
map_in_array.set('key', 'value');
let set1 = new Set(["1", 2, 3.0, 4]);

m2.set('array_here', [map_in_array, "Hello", true, 0.1, null, undefined, Number.POSITIVE_INFINITY, {
  "a": 4
}]);
m2.set('a set: ', set1);
const test = {
  "hello": "ok",
  "map": m
};

console.log(value_to_json(test));
0
hobbit_be