javaScript: Parsing a stringified object with JSON.parse removes references - javascript

I am trying to deep-clone an object, say "a" with k = JSON.parse(JSON.stringify(a)). It is important that I use the stringify way, since I am trying to save the object into a file and then load from it.
I stumbled upon a problem with references on the cloned object which is illustrated below:
var obj={};
obj.importantProperty={s:2};
obj.c=obj.importantProperty;
obj.d=obj.importantProperty;
console.log( obj.c === obj.d ); // Returns true
var cloned = JSON.parse(JSON.stringify(obj));
console.log( cloned.c === cloned.d ); // Returns false
I need the references to be kept when using JSON.parse, in the above example they are not. In my project the object is much more complicated, but in the end it comes down to the example above.
Thanks in advance to anyone who helps me with this :)

The proper way to do something like this would be to store the common referenced object(s) separately and reference it by an ID.
For instance, you can hold your importantProperty objects in an array and use the index as the ID:
var importantProperties = [
{ s: 1 },
{ s: 2 },
{ s: 3 }
];
var obj = {};
obj.importantProperty = importantProperties[1];
obj.c = obj.importantProperty;
obj.d = obj.importantProperty;
Then when you stringify the object you replace the referenced object with its index:
var stringified = JSON.stringify(obj, function(key, value) {
if (key) {
return importantProperties.indexOf(value);
}
return value;
});
console.log(stringified);
// prints {"importantProperty":1,"c":1,"d":1}
And then when you parse you simply reverse the process to revive the references:
var parsed = JSON.parse(stringified, function(key, value) {
if (key) {
return importantProperties[value];
}
return value;
});
console.log(parsed.c === parsed.d && parsed.d === parsed.importantProperty);
// prints true
Now, the example above works for your example code under the assumption that all properties in obj is an object from the importantProperties array. If that's not the case and it's only certain properties that is an importantProperties object, you need to check for that when replacing/reviving.
Assuming only the "importantProperty", "c" and "d" properties are such objects:
if (['importantProperty', 'c', 'd'].includes(key)) instead of just if (key)
If this isn't good enough and you don't want the property name to have anything to do with whether or not the value is an importantProperties object, you'll need to indicate this in the value together with the identifier. Here's an example of how this can be done:
// Replacing
JSON.stringify(obj, function(k, value) {
if (importantProperties.includes(value)) {
return 'ImportantProperty['
+ importantProperties.indexOf(value)
+ ']';
}
return value;
});
// Reviving
JSON.parse(stringified, function(k, value) {
if (/^ImportantProperty\[\d+\]$/.test(value)) {
var index = Number( value.match(/\d+/)[0] );
return importantProperties[index];
}
return value;
});

It is impossible to achieve your desired result using JSON because JSON format can contain only a limited ammount of data types (http://json.org/) and when you stringify an object to JSON some information gets lost.
Probably there is some other kind of serialization technique, but I would recommend you to look for another approach to store data.

Related

Accessing an array inside of JSON object from reviver

I have a JSON object formatted like {"Foo": ["B","A","R"]}
I am trying to access the values of the array like this:
var json = '{"Foo": ["B","A","R"]}';
expression = JSON.Parse(json, function(key, value){
if(key == "Foo"){
console.log(value.length); // logs "3"
console.log(value[1]); // logs "undefined"
}
});
If I ask for the length of value it returns the correct length of the array, but if I ask for the value it returns undefined and I am not quite sure why.There are other values in the JSON that I am able to access just fine, but they are not arrays. Any insight would be appreciated. Thanks!
You should use JSON.parse like this:
var json = '{"Foo":["B","A","R"]}';
var object = JSON.parse(json);
// object is now and object containing the data from 'json'
var expression = object["Foo"][1]; // object["Foo"] refers to the
// value with key "Foo"
(Calling JSON.parse with a callback parameter is an advanced feature for transforming the JSON object, not reading it. In almost all cases, though, you want to use it like the above code, with no callbacks.)
As mentioned in another answer, if you simply want to retrieve the second element of Foo, you can do that easily enough after parsing using standard property access techniques such as obj.Foo[1].
Assuming you really want to use the optional second "reviver" parameter to JSON.parse, you need to return the value from the "reviver" callback;
expression = JSON.Parse(json, function(key, value){
if (key == "Foo"){
console.log(value[1]);
}
return value;
^^^^^^^^^^^^^
});
The reason it appears you can't access value[1] but you can access value.length is (as mentioned by user663031) you don't have a return value.
The reviver function replaces one value with another, if no return is specified all functions will return undefined. The order the reviver receives the values is: first each of the values in the array separately, then the array.
In your code each value has already been replaced with "undefined", so the array has three undefined values as reported by the length. value[1] really is returning the value at position 1 but it is set to "undefined".
When the json string has arrays the reviver function is called with index, [Object] as key, value parameters .
This sniped of code that filter object properties on parse phase will be helpful:
var json = '{"_embedded": [{"a":"A","b":"B","links": {"c":"C"}},{"a":"A2", "b":"B2","links": {"c":"C2"}}]}';
var schemaNames=["_embedded", "a"];
var result= JSON.parse(json, function(k, v) {
console.log(k,v);
if ("" === k){
return v;
}
// On arrays k is a number
else if (schemaNames.indexOf(k) > -1 || !isNaN(k)){
return v;
}
});
console.log(JSON.stringify(result));
Output: {"_embedded":[{"a":"A"},{"a":"A2"}]}
https://jsfiddle.net/pdorgambide/vphbmtk1/
use this code
var json = {'foo' : ['B', 'A', 'R']};
$.each(json, function(key, value){if(key == 'foo'){console.log(value[1]);}});
you already have a json object so no need to parse it again.

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
Unfortunatly JSON.stringify(a); simply returns '{}', which means a becomes an empty object when restored.
I found es6-mapify that allows up/down-casting between a Map and a plain object, so that might be one solution, but I was hoping I would need to resort to an external dependency simply to persist my map.
Assuming that both your keys and your values are serialisable,
localStorage.myMap = JSON.stringify(Array.from(map.entries()));
should work. For the reverse, use
map = new Map(JSON.parse(localStorage.myMap));
Clean as a whistle:
JSON.stringify([...myMap])
Usually, serialization is only useful if this property holds
deserialize(serialize(data)).get(key) ≈ data.get(key)
where a ≈ b could be defined as serialize(a) === serialize(b).
This is satisfied when serializing an object to 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 :)
And this works because properties can only be strings, which can be losslessly serialized into strings.
However, ES6 maps allow arbitrary values as keys. This is problematic because, objects are uniquely identified by their reference, not their data. And when serializing objects, you lose the references.
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 :(
So I would say in general it's not possible to do it in an useful way.
And for those cases where it would work, most probably you can use a plain object instead of a map. This will also have these advantages:
It will be able to be stringified to JSON without losing key information.
It will work on older browsers.
It might be faster.
Building off of Oriol's answer, we can do a little better. We can still use object references for keys as long as the there is primitive root or entrance into the map, and each object key can be transitively found from that root key.
Modifying Oriol's example to use Douglas Crockford's JSON.decycle and JSON.retrocycle we can create a map that handles this case:
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 and retrocycle make it possible to encode cyclical structures and dags in JSON. This is useful if we want to build relations between objects without creating additional properties on those objects themselves, or want to interchangeably relate primitives to objects and visa-versa, by using an ES6 Map.
The one pitfall is that we cannot use the original key object for the new map (map3.get(key); would return undefined). However, holding the original key reference, but a newly parsed JSON map seems like a very unlikely case to ever have.
If you implement your own toJSON() function for any class objects you have then just regular old JSON.stringify() will just work!
Maps with Arrays for keys? Maps with other Map as values? A Map inside a regular Object? Maybe even your own custom class; easy.
Map.prototype.toJSON = function() {
return Array.from(this.entries());
};
That's it!
prototype manipulation is required here. You could go around adding toJSON() manually to all your non-standard stuff, but really you're just avoiding the power of JS
DEMO
test = {
regular : 'object',
map : new Map([
[['array', 'key'], 7],
['stringKey' , new Map([
['innerMap' , 'supported'],
['anotherValue', 8]
])]
])
};
console.log(JSON.stringify(test));
outputs:
{"regular":"object","map":[[["array","key"],7],["stringKey",[["innerMap","supported"],["anotherValue",8]]]]}
Deserialising all the way back to real Maps isn't as automatic, though. Using the above resultant string, I'll remake the maps to pull out a value:
test2 = JSON.parse(JSON.stringify(test));
console.log((new Map((new Map(test2.map)).get('stringKey'))).get('innerMap'));
outputs
"supported"
That's a bit messy, but with a little magic sauce you can make deserialisation automagic too.
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
;
};
Now the JSON is
{"regular":"object","test":["window.Map",[[["array","key"],7],["stringKey",["window.Map",[["innerMap","supported"],["anotherValue",8]]]]]]}
And deserialising and use is dead simple with our Map.fromJSON
test2 = JSON.parse(JSON.stringify(test), Map.fromJSON);
console.log(test2.map.get('stringKey').get('innerMap'));
outputs (and no new Map()s used)
"supported"
DEMO
The accepted answer will fail when you have multi dimentional Maps. One should always keep in mind that, a Map object can take another Map object as a key or value.
So a better and safer way of handling this job could be as follows;
function arrayifyMap(m){
return m.constructor === Map ? [...m].map(([v,k]) => [arrayifyMap(v),arrayifyMap(k)])
: m;
}
Once you have this tool then you can always do like;
localStorage.myMap = JSON.stringify(arrayifyMap(myMap))
// 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;
}
I wrote here the explanation about replacer and reviver functions here https://stackoverflow.com/a/56150320/696535
This code will work for any other value like regular JSON.stringify so there's no assumption that the serialised object must be a Map. It can also be a Map deeply nested in an array or an object.
One thing that is being left outis that Map is an ORDERED structure - i.e. when iterating the first item entered would be the first listed.
This is NOT like a Javascript Object. I required this type of structure (so i used Map) and then to find out that JSON.stringify doesn't work is painful (but understandable).
I ended up making a 'value_to_json' function, which means parsing EVERYTHING -
using JSON.stringify only for the most basic 'types'.
Unfortunately subclassing MAP with a .toJSON() doesn't work as it excepts a value not a JSON_string. Also it is considered legacy.
My use case would be exceptional though.
related:
https://github.com/DavidBruant/Map-Set.prototype.toJSON/issues/16
JSON left out Infinity and NaN; JSON status in ECMAScript?
How to stringify objects containing ES5 Sets and Maps?
JSON stringify a Set
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));
js use the localStorage API to store the ES6 Map
bug
"[object Map]" ❌
(() => {
const map = new Map();
map.set(1, {id: 1, name: 'eric'});
// Map(1) {1 => {…}}
// ❌
localStorage.setItem('app', map);
localStorage.getItem('app');
// "[object Map]"
})();
solution
use JSON.stringify to serialize the Map object before storing it and then use JSON.parse to deserialize it before access the the Map object ✅
(() => {
const map = new Map();
map.set(1, {id: 1, name: 'eric'});
// Map(1) {1 => {…}}
// ✅
localStorage.setItem('app', JSON.stringify([...map]));
const newMap = new Map(JSON.parse(localStorage.getItem('app')));
// Map(1) {1 => {…}}
})();
screenshots
refs
https://www.cnblogs.com/xgqfrms/p/14431425.html
It's important to remember that if you try to setItem on a huge map collection, it will throw Quota Exceeded Error. I tried persisting to local storage a map with 168590 entries and got this error. :(

Replacing underscore or lodash _.each with vanilla for loop

I'm wanting to replace the following code to no longer rely on the _.each() function of underscore.js or lodash.js:
function fset(data) {
_.each(dataDefault, function(item, key) {
var val = ( data[key] ? data[key] : dataDefault[key] );
$rootScope.meta[key] = val;
});
};
Ideally, I want to use a vanilla JavaScript for loop, but I don't understand how the _.each() function in underscore/lodash works to replace it...
Something like:
for(var i=0; i<data.length;i++) {
var val = ( data[key] ? data[key] : dataDefault[key] );
$rootScope.meta[key] = val;
}
But, I don't know how to get the key and item in this way...
dataDefault looks like:
var dataDefault = {
title: null,
description: null
};
An example of calling the function would be:
meta.fset({
title: 'Hello world',
description: 'DESC'
});
Try this:
Object.keys(dataDefault).forEach(function (key) {
var value = dataDefault[key]
// iteration code
})
With for..in you have to use hasOwnProperty to exclude inherit properties.
So, if I'm interpreting your logic correctly, what you're trying to do is loop through the keys in your defaults object, and if the object you're inspecting doesn't have that key, you want to add that key to the object and assign its value to the default value, is that correct? Any limitations on browser level?
The quickest way to do it if you know for sure what the default data object looks like would be to use a for..in loop:
var data = {}; // or wherever you get it from)
for (var key in defaultData){
data[key] = data[key] || defaultData[key];
}
That assumes that data.key is non-null and non-false. If false or null is a valid value (and your default is not null or false), then you'll want to make a bit more effort at ascertaining the existence of the key and type of the value. But based on your example, you're not worried about that.

Count number of object in another object , javascript-json

There seems to have many question asked similar on counting number of element already but I am failing to implement them with mine problem.
After jquery ajax I get JSON data returned which looks something like this
Object {0: Object, 1: Object , xxxx:"asdf" ,yyyy:"asdf", zzzz:"asdf"}
I want to get number of object between this { } braces ( not counting those xxx,yyy element )
I tried .length which doesn't work
I also tried using this Length of a JavaScript object but that return the number of element in each object. I just want the number of object
Thank You
Try this:
var json = { 0: {}, 1: {}, xxxx: "asdf", yyyy: "asdf", zzzz: "asdf" };
function typeOf( obj ) {
return ({}).toString.call( obj )
.match(/\s([a-zA-Z]+)/)[1].toLowerCase();
}
var total = 0;
for ( var o in json ) {
if ( typeOf( json[o] ) == 'object' ) {
total++;
}
}
console.log( total ); //=> 2
Everything is an object in JavaScript. The typeof operator is misleading and won't work in this case. You can use the typeOf function above that I extracted from this blog post: Fixing the JavaScript typeof operator (worth reading). There are other ways of doing it but this seems like the most straightforward.
If it's not just a coincidence that the objects are the ones with numeric property names, and the numeric properties count up sequentially, you could do something like this:
var obj = { /* your object here */ }
for (var i=0; i in obj; i++) {
// use obj[i] for something
}
// i is now equal to the number of numeric properties
This works because as soon as i is high enough that you've run out of properties the in operator will return false. Feel free to use .hasOwnProperty() instead if you prefer.
Obviously this is a specialised solution that doesn't test the type of the different properties at all. (To actually test the type see elclanrs' solution - and either way read the page he linked to.)
Say that the entire json is in a variable called json:
var total_objects = 0;
$.each(json, function () {
if (typeof this == 'object') {
total_objects++;
}
});
However, I am curious as to why you would need to know this.
You can use a customized version from the code of this question Length of Javascript Object (ie. Associative Array) and check for element's type using typeof operator and count only those which are an object (or an array).
Object.sizeObj = function(obj) {
var size = 0, key;
for (key in obj) {
if (typeof key[obj] === 'object' && obj.hasOwnProperty(key)) size++;
}
return size;
};
// Get the count of those elements which are an object
var objectsCount = Object.sizeObj(myArray);

Storing Tree Structures in JSON

I'm trying to write a system where the client (browser-based) requests a tree to be returned from the server (app engine, but that's irrelevant). The problem is in converting the tree to JSON: because each object refers to the objects 'below' it, when stringified I end up with an extremely long string which, on parsing, creates new objects for each child instead of references to other nodes.
My current solution would be to write an 'equalTo' and 'toString' function (converting object references to strings) stringify the resulting array and then recreate it on the client side by resolving strings to objects. That solution is making my terrible-algorithm-senses tingle, though, there must be a better way to return such structures through JSON!
EDIT: it just occurred to me that object references could also be converted to array indexes. It's a better solution, but still has that niggling bad-code feel to it.
EDIT2: Right, so I suppose some pseudo-code is in order, then.
var node = {
children : null;
};
var root = Object.create(node);
var level1a = Object.create(node);
var level1b = Object.create(node);
var level2a = Object.create(node);
var level2b = Object.create(node);
root.children = [level1a, level1b];
level1a.children = [level2a, level2b];
So you end up with a tree that looks like this:
stackoverflow won't let me post images
If you have a way to address nodes, you can use JSON revivers and replacers to convert between addresses and references.
For example, if you have two functions like
function addressForNode(node) { ... }
function nodeForAddress(address) { ... }
you could use revivers and replacers that call those when parsing or stringifying
var jsonString = JSON.stringify(tree, function (key, value) {
if (typeof value === 'object') {
var address = addressForNode(value);
if (address !== null) { // value really is a node
return { address: address }
}
}
return value;
});
// And to parse...
var tree = JSON.parse(jsonString, function (key, value) {
if (typeof value === 'object' && typeof value.address === 'string') {
return nodeForAddress(value.address);
}
return value;
});
If you can help it, don't restrict yourself to JSON. Instead consider generating JavaScript code included via linked script, using JSONP techniques if necessary.

Categories