javascript - Hur gör man för att JSON.stringify en ES6 Map?


Jag skulle vilja börja använda ES6 Map istället för JS-objekt, men jag hålls tillbaka eftersom jag inte kan lista ut hur man JSON.stringify() en Map. Mina nycklar är garanterat strängar och mina värden kommer alltid att listas. Måste jag verkligen skriva en omslagsmetod för att serialisera?

Svar #1

Både JSON.stringify och JSON.parse stöder ett andra argument. replacer och reviver respektive. Med replacer och reviver nedan är det möjligt att lägga till stöd för det inbyggda Map-objektet, inklusive djupt nästlade värden

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

Användning:

const originalValue = new Map([['a', 1]]);
const str = JSON.stringify(originalValue, replacer);
const newValue = JSON.parse(str, reviver);
console.log(originalValue, newValue);

Djup nästling med kombination av arrayer, objekt och mappar

const originalValue = [
  new Map([['a', {
    b: {
      c: new Map([['d', 'text']])
    }
  }]])
];
const str = JSON.stringify(originalValue, replacer);
const newValue = JSON.parse(str, reviver);
console.log(originalValue, newValue);

Svar #2

Du kan inte direkt omvandla Map-instansen till en sträng eftersom den inte har några egenskaper, men du kan konvertera den till en array av tupler:

jsonText = JSON.stringify(Array.from(map.entries()));

För omvändningen, använd

map = new Map(JSON.parse(jsonText));

Svar #3

Du kan inte.

Nycklarna i en karta kan vara vad som helst, inklusive objekt. Men JSON-syntax tillåter endast strängar som nycklar. Så det är omöjligt i allmänhet.

Mina nycklar är garanterat strängar och mina värden kommer alltid att vara listor

I det här fallet kan du använda ett vanligt objekt. Det kommer att ha dessa fördelar:

  • Det kommer att kunna serialiseras till JSON.
  • Det kommer att fungera på äldre webbläsare.
  • Det kan vara snabbare.

Svar #4

Även om det ännu inte finns någon metod som tillhandahålls av ecmascript, kan detta fortfarande göras med JSON.stringify om du mappar Map till ett JavaScript-primitive. Här är det exempel på Map vi kommer att använda.

const map = new Map();
map.set('foo', 'bar');
map.set('baz', 'quz');

Gå till ett JavaScript-objekt

Du kan konvertera till JavaScript-objektliteral med följande hjälpfunktion.

const mapToObj = m => {
  return Array.from(m).reduce((obj, [key, value]) => {
    obj[key] = value;
    return obj;
  }, {});
};
    
JSON.stringify(mapToObj(map)); // '{"foo":"bar","baz":"quz"}'

Gå till en JavaScript-array med objekt

Hjälpfunktionen för den här skulle vara ännu mer kompakt

const mapToAoO = m => {
  return Array.from(m).map( ([k,v]) => {return {[k]:v}} );
};
    
JSON.stringify(mapToAoO(map)); // '[{"foo":"bar"},{"baz":"quz"}]'

Gå till array av arrayer

Detta är ännu enklare, du kan bara använda

JSON.stringify( Array.from(map) ); // '[["foo","bar"],["baz","quz"]]'

Svar #5

Med hjälp av spread-syntax kan Map serialiseras på en rad:

JSON.stringify([...new Map()]);

och deserialisera det med:

let map = new Map(JSON.parse(map));

Svar #6

Med tanke på att ditt exempel är ett enkelt användningsfall där nycklarna kommer att vara enkla typer, tror jag att detta är det enklaste sättet att JSON-strängifiera en Map.

JSON.stringify(Object.fromEntries(map));

Sättet jag tänker på den underliggande datastrukturen för en Map är som en array av nyckel-värde-par (som själva är arrayer). Så, något i stil med detta:

const myMap = new Map([
     ["key1", "value1"],
     ["key2", "value2"],
     ["key3", "value3"]
]);

Eftersom den underliggande datastrukturen är det vi hittar i Object.entries, kan vi använda den inbyggda JavaScript-metoden Object.fromEntries() på en Map precis som vi skulle göra på en Array:

Object.fromEntries(myMap);

/*
{
     key1: "value1",
     key2: "value2",
     key3: "value3"
}
*/

Och sedan är allt du har kvar att göra att använda JSON.stringify() på resultatet av det.

Svar #7

Korrekt rundresningsserialisering

Bara kopiera detta och använd det. Eller använd npm-paketet.

const serialize = (value) => JSON.stringify(value, stringifyReplacer);
const deserialize = (text) => JSON.parse(text, parseReviver);

// License: CC0
function stringifyReplacer(key, value) {
  if (typeof value === "object" && value !== null) {
    if (value instanceof Map) {
      return {
        _meta: { type: "map" },
        value: Array.from(value.entries()),
      };
    } else if (value instanceof Set) { // bonus feature!
      return {
        _meta: { type: "set" },
        value: Array.from(value.values()),
      };
    } else if ("_meta" in value) {
      // Escape "_meta" properties
      return {
        ...value,
        _meta: {
          type: "escaped-meta",
          value: value["_meta"],
        },
      };
    }
  }
  return value;
}

function parseReviver(key, value) {
  if (typeof value === "object" && value !== null) {
    if ("_meta" in value) {
      if (value._meta.type === "map") {
        return new Map(value.value);
      } else if (value._meta.type === "set") {
        return new Set(value.value);
      } else if (value._meta.type === "escaped-meta") {
        // Un-escape the "_meta" property
        return {
          ...value,
          _meta: value._meta.value,
        };
      } else {
        console.warn("Unexpected meta", value._meta);
      }
    }
  }
  return value;
}

Prestanda?

Det finns en version som är lika hög kvalitet, men har bättre prestanda (testad i Chrome och Firefox). Om det är viktigt för dig, vänligen kolla in den!

https://stackoverflow.com/a/79016027/3492994

Varför är detta svårt?

Det borde vara möjligt att mata in vilken typ av data som helst, få giltig JSON och därifrån korrekt återskapa inmatningen.

Detta innebär att hantera

  • Kartor som har objekt som nycklar new Map([ [{cat:1}, "value"] ]). Detta betyder att vilket svar som helst som använder Object.fromEntries troligen är fel.
  • Kartor som har inbäddade kartor new Map([ ["key", new Map([ ["nested key", "nested value"] ])] ]). Många svar kringgår detta genom att bara besvara frågan och inte hantera något bortom det.
  • Blanda objekt och kartor {"key": new Map([ ["nested key", "nested value"] ]) }.

Och ovanpå dessa svårigheter måste serialiseringsformatet vara entydigt. Annars kan man inte alltid rekonstruera indata. Det bästa svaret har ett misslyckat testfall, se nedan.

Därför skrev jag denna förbättrade version. Den använder _meta istället för dataType, för att göra konflikter mer sällsynta och om en konflikt skulle uppstå, hanterar den den faktiskt entydigt. Förhoppningsvis är koden också tillräckligt enkel för att enkelt kunna utökas för att hantera andra behållare.

Mitt svar försöker däremot inte hantera extremt förbannelse fall, såsom en karta med objekt-egenskaper.

Ett testfall för mitt svar, som visar några kantfall

const originalValue = [
  new Map([['a', {
    b: {
      _meta: { __meta: "cat" },
      c: new Map([['d', 'text']])
    }
  }]]),
 { _meta: { type: "map" }}
];

console.log(originalValue);
let text = JSON.stringify(originalValue, stringifyReplacer);
console.log(text);
console.log(JSON.parse(text, parseReviver));

Accepterat svar rundreser inte

Det accepterade svaret är verkligen trevligt. Det fungerar dock inte när ett objekt med en dataType-egenskap skickas till det. Det kan göra det farligt att använda i vissa situationer, såsom

  1. JSON.stringify(data, acceptedAnswerReplacer) och skicka det över nätverket.
  2. Naiv nätverkshanterare avkodar det automatiskt som JSON. Från denna punkt framåt kan du inte säkert använda det accepterade svaret med de avkodade uppgifterna, eftersom det skulle orsaka många smygande problem.

Detta svar använder ett något mer komplext system för att åtgärda sådana problem.

// Test case for the accepted answer
const originalValue = { dataType: "Map" };
const str = JSON.stringify(originalValue, replacer);
const newValue = JSON.parse(str, reviver);
console.log(originalValue, str, newValue); 
// > Object { dataType: "Map" } , Map(0)
// Notice how the input was changed into something different

Svar #8

En bättre lösning

    // somewhere...
    class Klass extends Map {

        toJSON() {
            var object = { };
            for (let [key, value] of this) object[key] = value;
            return object;
        }

    }

    // somewhere else...
    import { Klass as Map } from '@core/utilities/ds/map';  // <--wherever "somewhere" is

    var map = new Map();
    map.set('a', 1);
    map.set('b', { datum: true });
    map.set('c', [ 1,2,3 ]);
    map.set( 'd', new Map([ ['e', true] ]) );

    var json = JSON.stringify(map, null, '\t');
    console.log('>', json);

Utdata

    > {
        "a": 1,
        "b": {
            "datum": true
        },
        "c": [
            1,
            2,
            3
        ],
        "d": {
            "e": true
        }
    }

Hoppas att det är mindre pinsamt än svaren ovan.

Svar #9

Stränglägg en Map-instans (objekt som nycklar är OK):

JSON.stringify([...map])

eller

JSON.stringify(Array.from(map))

eller

JSON.stringify(Array.from(map.entries()))

utdataformat:

// [["key1","value1"],["key2","value2"]]

Relaterade artiklar