如何在本地存储(或其他地方)中保存 ES6 映射?
How do I persist a ES6 Map in localstorage (or elsewhere)?
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
不幸的是JSON.stringify(a);
只是returns'{}',这意味着a在恢复时变成了一个空对象。
我发现 es6-mapify 允许在 Map 和普通对象之间使用 up/down-casting,所以这可能是一个解决方案,但我希望我需要求助于外部依赖项来坚持我的地图。
假设您的键和值都是可序列化的,
localStorage.myMap = JSON.stringify(Array.from(map.entries()));
应该可以。相反,使用
map = new Map(JSON.parse(localStorage.myMap));
通常,序列化只有在 属性 成立时才有用
deserialize(serialize(data)).get(key) ≈ data.get(key)
其中 a ≈ b
可以定义为 serialize(a) === serialize(b)
.
将对象序列化为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 :)
这是有效的,因为属性只能是字符串,可以无损地序列化为字符串。
但是,ES6 映射允许任意值作为键。这是有问题的,因为对象是由它们的引用而不是它们的数据唯一标识的。并且在序列化对象时,您会丢失引用。
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 :(
所以我会说一般来说不可能以有用的方式做到这一点。
对于那些可行的情况,您很可能可以使用普通对象而不是地图。这也会有这些优点:
- 将可以在不丢失关键信息的情况下被字符串化为JSON
- 它将在旧版浏览器上运行。
- 它可能会更快。
在 Oriol's 的基础上,我们可以做得更好。只要存在原始根或映射入口,我们仍然可以对键使用对象引用,并且可以从该根键传递地找到每个对象键。
修改 Oriol 的示例以使用 Douglas Crockford 的 JSON.decycle and JSON.retrocycle 我们可以创建一个地图来处理这种情况:
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 :)
Decycle 和 retrocycle 使得在 JSON 中编码循环结构和 dags 成为可能。如果我们想要在对象之间建立关系而不在这些对象本身上创建额外的属性,或者想要通过使用 ES6 Map 将基元与对象互换关联,反之亦然,这将很有用。
一个陷阱是我们不能 将原始键对象用于新地图(map3.get(key);
将return 未定义)。然而,持有原始关键参考,但新解析的 JSON 地图似乎不太可能出现。
当你有多维地图时,接受的答案将失败。人们应该始终牢记,一个 Map 对象可以将另一个 Map 对象作为键或值。
因此,处理这项工作的更好、更安全的方法如下;
function arrayifyMap(m){
return m.constructor === Map ? [...m].map(([v,k]) => [arrayifyMap(v),arrayifyMap(k)])
: m;
}
一旦你有了这个工具,你就可以随心所欲了;
localStorage.myMap = JSON.stringify(arrayifyMap(myMap))
被遗漏的一件事是 Map 是一个 ORDERED 结构——即当迭代时,第一个输入的项目将是第一个列出的。
这 不像 Javascript 对象。我需要这种类型的结构(所以我使用了 Map),然后发现 JSON.stringify 不起作用是痛苦的(但可以理解)。
我最终制作了一个 'value_to_json' 函数,这意味着解析一切 -
仅对最基本的 'types' 使用 JSON.stringify。
不幸的是,使用 .toJSON() 对 MAP 进行子类化不起作用,因为它除了值不是 JSON_string。它也被认为是遗产。
不过我的用例会很特别。
相关:
- https://github.com/DavidBruant/Map-Set.prototype.toJSON/issues/16
- JSON left out Infinity and NaN; JSON status in ECMAScript?
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));
一尘不染:
JSON.stringify([...myMap])
如果您为任何 class
对象实现自己的 toJSON()
函数,那么 普通的 JSON.stringify()
就可以了!
Map
s 和 Array
s 键? Map
s 与其他 Map
作为值?常规 Object
中的 Map
?甚至你自己的习惯 class;简单。
Map.prototype.toJSON = function() {
return Array.from(this.entries());
};
就是这样!
此处需要原型操作。您可以手动向所有非标准内容添加 toJSON()
,但实际上您只是在避免 JS
的强大功能
test = {
regular : 'object',
map : new Map([
[['array', 'key'], 7],
['stringKey' , new Map([
['innerMap' , 'supported'],
['anotherValue', 8]
])]
])
};
console.log(JSON.stringify(test));
输出:
{"regular":"object","map":[[["array","key"],7],["stringKey",[["innerMap","supported"],["anotherValue",8]]]]}
反序列化一直回到真正的 Map
s 并不是自动的,虽然。使用上面的结果字符串,我将重新制作地图以提取一个值:
test2 = JSON.parse(JSON.stringify(test));
console.log((new Map((new Map(test2.map)).get('stringKey'))).get('innerMap'));
输出
"supported"
这有点乱,但是 magic sauce 你也可以使反序列化变得自动化。
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
;
};
现在 JSON 是
{"regular":"object","test":["window.Map",[[["array","key"],7],["stringKey",["window.Map",[["innerMap","supported"],["anotherValue",8]]]]]]}
反序列化和使用我们的 Map.fromJSON
非常简单
test2 = JSON.parse(JSON.stringify(test), Map.fromJSON);
console.log(test2.map.get('stringKey').get('innerMap'));
输出(没有使用 new Map()
)
"supported"
// 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;
}
我在这里写了关于replacer和reviver功能的解释
此代码适用于任何其他值,如常规 JSON.stringify,因此不假设序列化对象必须是 Map。也可以是深度嵌套在数组或对象中的Map。
js 本地存储和地图合二为一
(() => {
// bug
const map = new Map();
map.set(1, {id: 1, name: 'eric'});
// Map(1) {1 => {…}}
localStorage.setItem('app', map);
// undefined
localStorage.getItem('app');
// "[object Map]" ❌
})();
(() => {
// ok
const map = new Map();
map.set(1, {id: 1, name: 'eric'});
// Map(1) {1 => {…}}
localStorage.setItem('app', JSON.stringify([...map]));
// undefined
const newMap = new Map(JSON.parse(localStorage.getItem('app')));
// Map(1) {1 => {…}} ✅
})();
截图
参考资料
重要的是要记住,如果您尝试在一个巨大的地图集合上设置 Item,它会抛出 Quota Exceeded Error
。我尝试将包含 168590 个条目的地图保存到本地存储并收到此错误。 :(
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
不幸的是JSON.stringify(a);
只是returns'{}',这意味着a在恢复时变成了一个空对象。
我发现 es6-mapify 允许在 Map 和普通对象之间使用 up/down-casting,所以这可能是一个解决方案,但我希望我需要求助于外部依赖项来坚持我的地图。
假设您的键和值都是可序列化的,
localStorage.myMap = JSON.stringify(Array.from(map.entries()));
应该可以。相反,使用
map = new Map(JSON.parse(localStorage.myMap));
通常,序列化只有在 属性 成立时才有用
deserialize(serialize(data)).get(key) ≈ data.get(key)
其中 a ≈ b
可以定义为 serialize(a) === serialize(b)
.
将对象序列化为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 :)
这是有效的,因为属性只能是字符串,可以无损地序列化为字符串。
但是,ES6 映射允许任意值作为键。这是有问题的,因为对象是由它们的引用而不是它们的数据唯一标识的。并且在序列化对象时,您会丢失引用。
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 :(
所以我会说一般来说不可能以有用的方式做到这一点。
对于那些可行的情况,您很可能可以使用普通对象而不是地图。这也会有这些优点:
- 将可以在不丢失关键信息的情况下被字符串化为JSON
- 它将在旧版浏览器上运行。
- 它可能会更快。
在 Oriol's
修改 Oriol 的示例以使用 Douglas Crockford 的 JSON.decycle and JSON.retrocycle 我们可以创建一个地图来处理这种情况:
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 :)
Decycle 和 retrocycle 使得在 JSON 中编码循环结构和 dags 成为可能。如果我们想要在对象之间建立关系而不在这些对象本身上创建额外的属性,或者想要通过使用 ES6 Map 将基元与对象互换关联,反之亦然,这将很有用。
一个陷阱是我们不能 将原始键对象用于新地图(map3.get(key);
将return 未定义)。然而,持有原始关键参考,但新解析的 JSON 地图似乎不太可能出现。
当你有多维地图时,接受的答案将失败。人们应该始终牢记,一个 Map 对象可以将另一个 Map 对象作为键或值。
因此,处理这项工作的更好、更安全的方法如下;
function arrayifyMap(m){
return m.constructor === Map ? [...m].map(([v,k]) => [arrayifyMap(v),arrayifyMap(k)])
: m;
}
一旦你有了这个工具,你就可以随心所欲了;
localStorage.myMap = JSON.stringify(arrayifyMap(myMap))
被遗漏的一件事是 Map 是一个 ORDERED 结构——即当迭代时,第一个输入的项目将是第一个列出的。
这 不像 Javascript 对象。我需要这种类型的结构(所以我使用了 Map),然后发现 JSON.stringify 不起作用是痛苦的(但可以理解)。
我最终制作了一个 'value_to_json' 函数,这意味着解析一切 - 仅对最基本的 'types' 使用 JSON.stringify。
不幸的是,使用 .toJSON() 对 MAP 进行子类化不起作用,因为它除了值不是 JSON_string。它也被认为是遗产。
不过我的用例会很特别。
相关:
- https://github.com/DavidBruant/Map-Set.prototype.toJSON/issues/16
- JSON left out Infinity and NaN; JSON status in ECMAScript?
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));
一尘不染:
JSON.stringify([...myMap])
如果您为任何 class
对象实现自己的 toJSON()
函数,那么 普通的 JSON.stringify()
就可以了!
Map
s 和 Array
s 键? Map
s 与其他 Map
作为值?常规 Object
中的 Map
?甚至你自己的习惯 class;简单。
Map.prototype.toJSON = function() {
return Array.from(this.entries());
};
就是这样!
此处需要原型操作。您可以手动向所有非标准内容添加 toJSON()
,但实际上您只是在避免 JS
test = {
regular : 'object',
map : new Map([
[['array', 'key'], 7],
['stringKey' , new Map([
['innerMap' , 'supported'],
['anotherValue', 8]
])]
])
};
console.log(JSON.stringify(test));
输出:
{"regular":"object","map":[[["array","key"],7],["stringKey",[["innerMap","supported"],["anotherValue",8]]]]}
反序列化一直回到真正的 Map
s 并不是自动的,虽然。使用上面的结果字符串,我将重新制作地图以提取一个值:
test2 = JSON.parse(JSON.stringify(test));
console.log((new Map((new Map(test2.map)).get('stringKey'))).get('innerMap'));
输出
"supported"
这有点乱,但是 magic sauce 你也可以使反序列化变得自动化。
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
;
};
现在 JSON 是
{"regular":"object","test":["window.Map",[[["array","key"],7],["stringKey",["window.Map",[["innerMap","supported"],["anotherValue",8]]]]]]}
反序列化和使用我们的 Map.fromJSON
test2 = JSON.parse(JSON.stringify(test), Map.fromJSON);
console.log(test2.map.get('stringKey').get('innerMap'));
输出(没有使用 new Map()
)
"supported"
// 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;
}
我在这里写了关于replacer和reviver功能的解释
此代码适用于任何其他值,如常规 JSON.stringify,因此不假设序列化对象必须是 Map。也可以是深度嵌套在数组或对象中的Map。
js 本地存储和地图合二为一
(() => {
// bug
const map = new Map();
map.set(1, {id: 1, name: 'eric'});
// Map(1) {1 => {…}}
localStorage.setItem('app', map);
// undefined
localStorage.getItem('app');
// "[object Map]" ❌
})();
(() => {
// ok
const map = new Map();
map.set(1, {id: 1, name: 'eric'});
// Map(1) {1 => {…}}
localStorage.setItem('app', JSON.stringify([...map]));
// undefined
const newMap = new Map(JSON.parse(localStorage.getItem('app')));
// Map(1) {1 => {…}} ✅
})();
截图
参考资料
重要的是要记住,如果您尝试在一个巨大的地图集合上设置 Item,它会抛出 Quota Exceeded Error
。我尝试将包含 168590 个条目的地图保存到本地存储并收到此错误。 :(