【问题标题】:How do you JSON.stringify an ES6 Map?你如何 JSON.stringify 一个 ES6 地图?
【发布时间】:2023-01-23 23:27:28
【问题描述】:

我想开始使用 ES6 Map 而不是 JS 对象,但我被阻止了,因为我不知道如何使用 JSON.stringify()Map。我的键保证是字符串,我的值将始终列出。我真的必须编写一个包装器方法来序列化吗?

【问题讨论】:

  • 关于该主题的有趣文章2ality.com/2015/08/es6-map-json.html
  • 我能够让它工作。结果在embed.plnkr.co/oNlQQBDyJUiIQlgWUPVP 的 Plunkr 上。该解决方案使用 JSON.stringify(obj, replacerFunction) 检查是否正在传递 Map 对象并将 Map 对象转换为 Javascript 对象(即 JSON.stringify)然后将转换为字符串。
  • 如果您的键保证是字符串(或数字)并且您的值数组,你可以做类似[...someMap.entries()].join(';')的事情;对于更复杂的事情,您可以使用[...someMap.entries()].reduce((acc, cur) => acc + `${cur[0]}:${/* do something to stringify cur[1] */ }`, '')之类的东西尝试类似的事情
  • @Oriol 如果键名可能与默认属性相同怎么办? obj[key] 可能会给你带来意想不到的东西。考虑案例if (!obj[key]) obj[key] = newList; else obj[key].mergeWith(newList);

标签: javascript json dictionary ecmascript-6


【解决方案1】:

JSON.stringifyJSON.parse 都支持第二个参数。 replacerreviver 分别。使用下面的 replacer 和 reviver,可以添加对原生 Map 对象的支持,包括深度嵌套的值

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;
}

用法:

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

结合数组、对象和映射的深度嵌套

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);

【讨论】:

  • 刚刚将其标记为正确。虽然我不喜欢您必须使用非标准化的dataType“弄脏”网络中的数据,但我想不出更简洁的方法。谢谢。
  • @Pawel 使用this[key] 而不是value 的原因是什么?
  • @JimiDini 好点,更新了。现在,如果有人想将它们声明为箭头函数,它不会弄乱作用域
  • 对我来说似乎有一个小问题:任何偶然具有属性 o.dataType==='Map' 的普通对象 o 也将在您对其进行序列化-反序列化时转换为 Map 。
  • @mkoe 当然可以,但是这种可能性介于被闪电击中和躲在地下室时被闪电击中之间
【解决方案2】:

您不能直接对 Map 实例进行字符串化,因为它没有任何属性,但您可以将其转换为元组数组:

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

相反,使用

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

【讨论】:

  • 这不会转换为 JSON 对象,而是转换为数组数组。不是一回事。请参阅下面 Evan Carroll 的回答以获得更完整的答案。
  • @SatThiru 元组数组是Maps 的习惯表示,它与构造函数和迭代器配合得很好。此外,它是具有非字符串键的映射的唯一合理表示,并且 object 不会在那里工作。
  • 如果 key 是一个对象,则 @Bergi Stringify 不起作用,例如"{"[object Object]":{"b":2}}" - 对象键是地图的主要功能之一
  • @Drenai 然后不要使用Obect.fromEntries,并使用我的主要答案中的代码而不是评论中的代码。构建对象文字的代码是对 Sat Thiru 的回应,他给出了键是字符串的情况。
【解决方案3】:

你不能。

地图的键可以是任何东西,包括对象。但是 JSON 语法只允许字符串作为键。所以在一般情况下是不可能的。

我的键保证是字符串,我的值将始终是列表

在这种情况下,您可以使用普通对象。它将具有以下优点:

  • 它将能够被字符串化为 JSON。
  • 它适用于旧版浏览器。
  • 它可能会更快。

【讨论】:

  • 对于好奇 - 在最新的 chrome 中,任何地图都序列化为“{}”
  • “它可能会更快”——你有这方面的资料吗?我在想象一个简单的哈希映射一定比一个完整的对象更快,但我没有证据。 :)
  • @Xplouder 该测试使用昂贵的hasOwnProperty。没有它,Firefox 迭代对象的速度比地图快得多。不过,地图在 Chrome 上仍然更快。 jsperf.com/es6-map-vs-object-properties/95
  • 的确,Firefox 45v 似乎比 Chrome +49v 更快地迭代对象。然而,在 Chrome 中,地图仍然胜过对象。
  • 由于这个,只是路过并找出我的问题。有时候,我真的很想搬到农场,把这一切都抛在脑后。
【解决方案4】:

虽然 ecmascript 还没有提供任何方法,但如果您将 Map 映射到 JavaScript 原语,仍然可以使用 JSON.stingify 来完成。这是我们将使用的样本Map

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

转到 JavaScript 对象

您可以使用以下辅助函数将其转换为 JavaScript 对象字面量。

const mapToObj = m => {
  return Array.from(m).reduce((obj, [key, value]) => {
    obj[key] = value;
    return obj;
  }, {});
};

JSON.stringify(mapToObj(map)); // '{"foo":"bar","baz":"quz"}'

转到 JavaScript 对象数组

这个的辅助函数会更紧凑

const mapToAoO = m => {
  return Array.from(m).map( ([k,v]) => {return {[k]:v}} );
};

JSON.stringify(mapToAoO(map)); // '[{"foo":"bar"},{"baz":"quz"}]'

转到数组数组

这更容易,你可以使用

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

【讨论】:

  • > 转到一个 JavaScript 对象 < 它不应该有代码来处理诸如 __proto__ 之类的键吗?或者您可以通过尝试序列化这样的地图来破坏整个环境。我相信 Alok 的回应不会受到影响。
  • 正如 Oriol 的回答所指出的,这是不正确的。映射键可以是对象,这个答案不处理。
【解决方案5】:

使用spread sytax Map 可以在一行中序列化:

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

并反序列化:

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

【讨论】:

  • 这适用于一维地图,但不适用于 n 维地图。
【解决方案6】:

鉴于您的示例是一个简单的用例,其中键将是简单类型,我认为这是对 Map 进行 JSON 字符串化的最简单方法。

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

我认为 Map 的底层数据结构的方式是作为键值对的数组(作为数组本身)。所以,像这样:

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

因为底层数据结构是我们在 Object.entries 中找到的,所以我们可以像在 Array 上一样在 Map 上使用 Object.fromEntries() 的原生 JavaScript 方法:

Object.fromEntries(myMap);

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

然后你剩下的就是在结果上使用 JSON.stringify() 。

【讨论】:

  • 这个很好,但确实需要你以 E​​S2019 为目标。
  • 小心,如果您想走一条路,这很好。Object.fromEntries()MDN Docs中所述,您可以将Map解析为Object但不能返回!它会抛出一个object is not iterable错误。
  • @Megajin Object.fromEntries() 是非破坏性的,所以你的原始地图仍然完好无损。
  • @AlokSomani 是的,你是对的。但是,如果你想解析一个 JSON(或新创建的对象)回来,它不会工作。
【解决方案7】:

字符串化 Map 实例(对象作为键是可以的):

JSON.stringify([...map])

或者

JSON.stringify(Array.from(map))

或者

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

输出格式:

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

【讨论】:

    【解决方案8】:

    更好的解决方案

        // 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, '	');
        console.log('>', json);
    

    输出

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

    希望这不像上面的答案那样令人毛骨悚然。

    【讨论】:

    • 我不确定很多人是否会对扩展核心地图类只是为了将其序列化为 json 感到满意......
    • 他们不一定是,但这是一种更可靠的方式。具体来说,这符合 SOLID 的 LSP 和 OCP 原则。也就是说,本机 Map 正在扩展,而不是修改,并且仍然可以将 Liskov 替换 (LSP) 与本机 Map 一起使用。诚然,它比许多新手或坚定的函数式编程人员更喜欢 OOP,但至少它被基本软件设计原则的可靠基准所困扰。如果你想实现 SOLID 的接口隔离原则 (ISP),你可以有一个小的 IJSONAble 接口(当然使用 TypeScript)。
    【解决方案9】:

    即使您有嵌套地图,以下解决方案也适用

    function stringifyMap(myMap) {
        function selfIterator(map) {
            return Array.from(map).reduce((acc, [key, value]) => {
                if (value instanceof Map) {
                    acc[key] = selfIterator(value);
                } else {
                    acc[key] = value;
                }
    
                return acc;
            }, {})
        }
    
        const res = selfIterator(myMap)
        return JSON.stringify(res);
    }
    

    【讨论】:

    • 在不测试您的答案的情况下,我已经意识到它如何引起人们对嵌套地图问题的关注。即使您成功将其转换为 JSON,将来进行的任何解析都必须明确知道 JSON 最初是一个 Map 并且(更糟糕的是)每个子地图(它包含)最初也是一个地图。否则,没有办法确定 array of pairs 不只是用来代替 Map 的。对象和数组的层次结构在解析时不会承担这种负担。 Map 的任何正确序列化都会明确表明它是 Map
    • 更多关于 here 的信息。
    【解决方案10】:

    非常简单的方法。

      const map = new Map();
      map.set('Key1', "Value1");
      map.set('Key2', "Value2");
      console.log(Object.fromEntries(map));
    

    ` 输出:-

    {"Key1": "Value1","Key2": "Value2"}

    【讨论】:

    • 警告:Map 可以将非字符串值作为键。如果您的 Map 键本身是不可字符串化的类型,这将不起作用:JSON.stringify(Object.fromEntries(new Map([['s', 'r'],[{s:3},'g']]))) 变为 '{"s":"r","[object Object]":"g"}'
    【解决方案11】:

    您不能在MapSet 上拨打JSON.stringify

    您将需要转换:

    • 使用Object.fromEntriesMap转化为原始的Object,或者
    • 使用扩展运算符[...]Set转化为原语Array

    …在致电JSON.stringify之前

    地图

    const
      obj = { 'Key1': 'Value1', 'Key2': 'Value2' },
      map = new Map(Object.entries(obj));
    
    map.set('Key3', 'Value3'); // Add a new entry
    
    // Does NOT show the key-value pairs
    console.log('Map:', JSON.stringify(map));
    
    // Shows the key-value pairs
    console.log(JSON.stringify(Object.fromEntries(map), null, 2));
    .as-console-wrapper { top: 0; max-height: 100% !important; }

    const
      arr = ['Value1', 'Value2'],
      set = new Set(arr);
    
    set.add('Value3'); // Add a new item
    
    // Does NOT show the values
    console.log('Set:', JSON.stringify(set));
    
    // Show the values
    console.log(JSON.stringify([...set], null, 2));
    .as-console-wrapper { top: 0; max-height: 100% !important; }

    toJSON方法

    如果你想在类对象上调用JSON.stringify,你需要覆盖toJSON方法来返回你的实例数据。

    class Cat {
      constructor(options = {}) {
        this.name = options.name ?? '';
        this.age = options.age ?? 0;
      }
      toString() {
        return `[Cat name="${this.name}", age="${this.age}"]`
      }
      toJSON() {
        return { name: this.name, age: this.age };
      }
      static fromObject(obj) {
        const { name, age } = obj ?? {};
        return new Cat({ name, age });
      }
    }
    
    /*
     * JSON Set adds the missing methods:
     * - toJSON
     * - toString
     */
    class JSONSet extends Set {
      constructor(values) {
        super(values)
      }
      toString() {
        return super
          .toString()
          .replace(']', ` ${[...this].map(v => v.toString())
          .join(', ')}]`);
      }
      toJSON() {
        return [...this];
      }
    }
    
    const cats = new JSONSet([
      Cat.fromObject({ name: 'Furball', age: 2 }),
      Cat.fromObject({ name: 'Artemis', age: 5 })
    ]);
    
    console.log(cats.toString());
    console.log(JSON.stringify(cats, null, 2));
    .as-console-wrapper { top: 0; max-height: 100% !important; }

    【讨论】:

    • 将 Maps 和 Sets 序列化为 JSON 有一种更简单的方法,因为 JSON.stringify 和 JSON.parse 有第二个参数,可以让您添加自定义规则。请参阅我的答案以获取在所有情况下都正确往返的答案。
    【解决方案12】:

    只想共享我的 Map 和 Set JSON.stringify 版本。 我正在对它们进行排序,对调试很有用......

    function replacer(key, value) {
        if (value instanceof Map) {
            const reducer = (obj, mapKey) => {
                obj[mapKey] = value.get(mapKey);
                return obj;
            };
            return [...value.keys()].sort().reduce(reducer, {});
        } else if (value instanceof Set) {
            return [...value].sort();
        }
        return value;
    }
    

    用法:

    const map = new Map();
    const numbers= new Set()
    numbers.add(3);
    numbers.add(2);
    numbers.add(3);
    numbers.add(1);
    const chars= new Set()
    chars.add('b')
    chars.add('a')
    chars.add('a')
    map.set("numbers",numbers)
    map.set("chars",chars)
    
    console.log(JSON.stringify(map, replacer, 2));
    

    结果:

    {
      "chars": [
        "a",
        "b"
      ],
      "numbers": [
        1,
        2,
        3
      ]
    }
    

    【讨论】:

      【解决方案13】:

      以下方法会将 Map 转换为 JSON 字符串:

      public static getJSONObj(): string {
          return JSON.stringify(Object.fromEntries(map));
      }
      

      例子:

      const x = new Map();
      x.set("SomeBool", true);
      x.set("number1", 1);
      x.set("anObj", { name: "joe", age: 22, isAlive: true });
      
      const json = getJSONObj(x);
      
      // Output:
      // '{"SomeBool":true,"number1":1,"anObj":{"name":"joe","age":222,"isAlive":true}}'
      

      【讨论】:

      • 这是正确的答案,除非我遗漏了什么。所有这些其他方法都可以从中获得绝对的一餐。
      • 与 Alok Somani 的回答相比,这个回答没有增加任何新内容。另外,它不处理嵌套地图。它也有与 Rakesh Singh Balhara 的回答相同的错误。
      【解决方案14】:

      正确的往返序列化

      只需复制并使用它。或者使用the npm package

      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;
      }
      

      为什么这很难?

      应该可以输入任何类型的数据,获取有效的 JSON,然后从那里获取正确地重建输入。

      这意味着处理

      • 以对象作为键的地图 new Map([ [{cat:1}, "value"] ])。这意味着任何使用 Object.fromEntries 的答案都可能是错误的。
      • 具有嵌套地图的地图 new Map([ ["key", new Map([ ["nested key", "nested value"] ])] ])。许多答案通过仅回答问题而不处理任何其他问题来回避这一点。
      • 混合对象和地图{"key": new Map([ ["nested key", "nested value"] ]) }

      除了这些困难之外,序列化格式必须是明确的。否则不能总是重建输入。最佳答案有一个失败的测试用例,见下文。

      因此,我写了这个改进版本。它使用 _meta 而不是 dataType 来减少冲突,如果确实发生冲突,它实际上会明确处理。希望代码也足够简单,可以轻松扩展以处理其他容器。

      然而,我的回答并没有试图处理极度被诅咒的情况,例如a map with object properties

      我的答案的测试用例,它演示了一些边缘情况

      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));
      

      接受的答案不是往返

      接受的答案真的很可爱。但是,当将具有 dataType 属性的对象传递给它时,它不会往返。

      // 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
      

      【讨论】:

        【解决方案15】:

        我真的不知道为什么这里有那么多长的aweser。这个简短的版本解决了我的问题:

        const data = new Map()
        data.set('visible', true)
        data.set('child', new Map())
        data.get('child').set('visible', false)
        
        const str = JSON.stringify(data, (_, v) => v instanceof Map ? Object.fromEntries(v) : v)
        // '{"visible":true,"child":{"visible":false}}'
        
        const recovered = JSON.parse(str, (_, v) => typeof v === 'object' ? new Map(Object.entries(v)) : v)
        // Map(2) { 'visible' => true, 'child' => Map(1) { 'visible' => false } }
        

        【讨论】:

        • 这会将对象变成地图,这不是人们想要的。试试const data = {visible: true, child: {visible: false}}。您将恢复两个嵌套映射而不是恢复对象。查看我的答案以获得真正正确的实施。
        【解决方案16】:

        尽管在某些情况下,如果您是地图的创建者,您会在单独的“src”文件中编写代码并将副本另存为 .txt 文件,如果编写得足够简洁,则可以轻松读取、破译, 并添加到服务器端。

        然后,新文件将保存为 .js 文件,并从服务器发回对它的引用。一旦以 JS 形式读回,该文件将完美地重建自身。美妙之处在于重建不需要骇人听闻的迭代或解析。

        【讨论】:

        • 您的答案可以通过其他支持信息得到改进。请edit 添加更多详细信息,例如引用或文档,以便其他人可以确认您的答案是正确的。你可以找到更多关于如何写出好的答案的信息in the help center
        • 这听起来像是一个不错的 10,000 英尺流程概述,但实际实施会更有用。
        • 嗯,这比一个完整的解决方案更值得深思。抱歉,我是新来的,还不确定如何将我的代码添加到 cmets。
        • 我不明白,为什么这与问题有关?
        猜你喜欢
        • 2015-05-19
        • 2015-09-14
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多