如何对ES6 Map进行JSON.stringify操作?

370

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


7
关于该主题的有趣文章:http://2ality.com/2015/08/es6-map-json.html - David Chase
我已经成功让它工作了。结果在Plunkr上,网址为http://embed.plnkr.co/oNlQQBDyJUiIQlgWUPVP/。该解决方案使用JSON.stringify(obj,replacerFunction),检查是否传递了Map对象,并将Map对象转换为JavaScript对象(然后JSON.stringify将其转换为字符串)。 - PatS
2
如果你的键保证是字符串(或数字),而你的值是数组,你可以像这样做[...someMap.entries()].join(';'); 对于更复杂的情况,你可以尝试类似的方法,使用[...someMap.entries()].reduce((acc, cur) => acc + \${cur[0]}:${/* do something to stringify cur[1] */ }`, '')`。 - user56reinstatemonica8
@Oriol 如果键名与默认属性相同,会怎样呢?obj[key]可能会得到一些意外的结果。考虑这种情况:if (!obj[key]) obj[key] = newList; else obj[key].mergeWith(newList); - Franklin Yu
即使MDN也建议使用这个Stack Overflow链接。 - undefined
16个回答

405

JSON.stringifyJSON.parse都支持第二个参数,分别为replacerreviver。通过使用这两个参数,可以为原生的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);

12
刚刚将这个标记为正确的。虽然我不喜欢通过非标准化的 dataType 在网络传输中"弄脏"数据,但我想不到更好的方法了。谢谢。 - rynop
2
@Pawel,为什么使用this[key]而不是value - JimiDini
2
在我看来,似乎存在一个小问题:任何具有属性o.dataType==='Map'的普通对象o,在序列化-反序列化时也会被转换为Map。 - mkoe
3
@mkoe 当然可以,但这种可能性就像被闪电击中并躲在地下室里被闪电击中之间的概率一样微乎其微。 - Pawel
2
@Stefnotch 我同意可能会有这样的情况发生,比如一个内部公司的npm包,它是一个api的包装器,可以传递一些默认的身份验证头选项并自动解析JSON以方便使用,例如 const getUrl = (url) => fetch(url, apiOptions).then(res => res.json()),其中 apiOptions 有一些全局设置。值得记住的是,这种情况可能会发生,必须正确地进行适应。 - Pawel
显示剩余18条评论

158

您无法直接将Map实例字符串化,因为它没有任何属性,但是您可以将其转换为元组数组:

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

要进行反向操作,请使用

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

16
这不会转换为一个JSON对象,而是转换为数组的数组。不是同一件事情。请参见下面Evan Carroll的答案以获得更完整的答案。 - Sat Thiru
12
元组数组是表示“映射”(Map)的惯用方式,它与构造函数和迭代器相配合。而且这也是具有非字符串键的映射的唯一明智的表示方法,使用对象就不行。 - Bergi
Bergi,请注意OP说:“我的键保证是字符串”。 - Sat Thiru
1
@Bergi 如果 key 是一个对象,例如 "{"[object Object]":{"b":2}}",则 Stringify 将无法工作 - 对象键是 Maps 的主要特征之一。 - Drenai
6
那就不要使用Object.fromEntries,而是使用我主要答案中的代码,而不是评论中的代码。构建对象字面量的代码是为了响应Sat Thiru的情况,其中键是字符串。 - Bergi

71

无法实现。

一个Map对象的键可以是任何类型,包括对象。但是在JSON语法中,键只能是字符串。因此一般情况下是不可能实现的。

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

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

  • 它可以被转换为JSON格式。
  • 它能够在较老版本的浏览器上运行。
  • 它可能会更快。

54
对于好奇者——在最新版的Chrome中,任何地图都会序列化为'{}'。 - Capaj
14
“它可能会更快” - 您有任何相关来源吗?我想象一个简单的哈希映射可能比完整的对象更快,但我没有证明。 :) - Lilleman
3
@Xplouder 那个测试使用了昂贵的hasOwnProperty。没有它,Firefox在迭代对象时比映射要快得多。不过,在Chrome上,映射仍然更快。http://jsperf.com/es6-map-vs-object-properties/95 - Oriol
5
只是路过并通过这篇文章解决了我的问题,谢谢。有时候我真的很希望搬到一个农场,把这一切都抛之脑后。 - napolux
3
尽管该答案确实指出了其中的棘手之处,但正如被接受的答案所表明的那样,它绝对不是“不可能的”。 - Stefnotch
显示剩余8条评论

47
虽然ECMAScript尚未提供相应的方法,但我们仍然可以使用JSON.stringifyMap映射为JavaScript原始类型来实现此功能。以下是我们将使用的示例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"]]'

1
进入 JavaScript 对象难道它不应该有处理 __proto__ 等键的代码吗?否则,尝试序列化这样的映射可能会损坏整个环境。我相信 Alok 的回答没有这个问题。 - roim
正如Oriol的回答所指出的那样,这是不正确的。Map键可以是对象,而这个答案没有处理这种情况。 - Stefnotch

35

使用扩展语法可以将Map序列化为一行:

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

使用以下方法进行反序列化:

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

15
这对于一维地图是可行的,但对于 n 维地图则不行。 - mattsven

28

考虑到你的示例是一个简单的用例,其中键将是简单类型,我认为这是将Map转换为JSON字符串最简单的方法。

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

我对 Map 的底层数据结构的想法是将其看作一组键值对数组(本身也是数组)。因此,类似于这样:

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

由于底层数据结构与Object.entries相同,我们可以像对待数组一样在Map上使用本地JavaScript方法 Object.fromEntries()

Object.fromEntries(myMap);

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

然后你只需要对其结果使用JSON.stringify()。


这个很好,但需要你针对ES2019进行目标定位。 - Procrastin8
1
小心,这只适用于单向转换。根据 MDN 文档,你可以将一个 Map 转换为 Object,但不能反过来!否则会抛出 object is not iterable 错误。 - Megajin
@Megajin Object.fromEntries() 是非破坏性的,因此您仍将保留原始的 Map。 - Alok Somani
1
@AlokSomani 是的,你说得对。但是如果你想要解析一个 JSON(或者新创建的对象),它就不起作用了。 - Megajin
1
仅供参考,对于能够确保整个映射结构(包括嵌套对象)具有字符串键的Typescript用户来说,这是完全可以的 Map<string, otherType> - Laercio Metzner

14

更好的解决方案

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

输出

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

希望这比上面的答案不那么尴尬。

我不确定有多少人会满意于仅仅为了将其序列化为 JSON 来扩展核心映射类。 - vasia
3
它们不一定非得这样做,但这是更符合SOLID原则的方式。具体来说,这与SOLID的LSP和OCP原则相一致。也就是说,扩展了本地Map,而不是修改了它,并且仍然可以使用本地Map进行Liskov替换(LSP)。当然,这比许多新手或坚定的函数式编程人员更符合面向对象编程(OOP)的风格,但至少它建立在基本软件设计原则的经验和真实基础之上。如果您想要实现SOLID的接口隔离原则(ISP),您可以使用一个小的IJSONAble接口(当然要使用TypeScript)。 - Cody

12

正确的往返序列化

只需复制并使用它,或者使用npm包

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,使冲突更少,如果发生冲突,它实际上可以无歧义地处理它。希望代码也足够简单,可以轻松地扩展到处理其他容器。

然而,我的答案不尝试处理极端恶劣的情况,例如具有对象属性的映射

下面是我的答案的一个测试用例,演示了一些边缘情况:

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 属性的对象时,它无法回传。在某些情况下,这可能会使其使用变得危险,例如:

  1. JSON.stringify(data, acceptedAnswerReplacer) 并通过网络发送。
  2. 简单的网络处理程序自动进行 JSON 解码。从此之后,您不能安全地使用接受的答案来处理解码后的数据,因为这样做会引起许多难以察觉的问题。

此答案使用了更复杂的方案来解决这些问题。

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

12

Map实例转换为字符串(键为对象也可以)


JSON.stringify([...map])
或者
JSON.stringify(Array.from(map))
或者
JSON.stringify(Array.from(map.entries()))

输出格式:

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

5
非常简单的方法。
  const map = new Map();
  map.set('Key1', "Value1");
  map.set('Key2', "Value2");
  console.log(Object.fromEntries(map));

{"键1": "值1","键2": "值2"}


6
警告:Map 可以有非字符串值作为键。如果您的 Map 键本身是无法字符串化的类型,则此方法将无法正常工作: JSON.stringify(Object.fromEntries(new Map([['s', 'r'],[{s:3},'g']]))) 的结果会变成 '{"s":"r","[object Object]":"g"}' - asp47

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接