Multiple destructuring assignments return undefined - javascript

TLDR Edit: I was confusing object destructuring w/ arrays and spread syntax. Below edit does the trick to recursively unpack the nested object.
let nextLevel = Object.values(obj)
return goingDeep(...nextLevel)
Following is the original question, which I'll leave up in case another noob such as myself runs into this, help save them from the downvotes ;p
Attempt to destructure a nested object recursively returns undefined. Putting that aside simply trying to destructure the same object into different variables returns undefined.
Keeping it simple, just assuming a single key:value (object) per layer so no need to iterate.
const container = {container1: {container2 : {container3: {container4: 'core'}}}}
Putting recursion aside for the moment the following results with simply two different destructuring assignments...
const {level1} = container
console.log(level1) // => container1: {container2:{ etc
const {level1_different} = container
console.log(level1_different) // => undefined
this is what I attempted with the recursion
const goingDeep = (obj) => {
if (obj.hasOwnProperty('container4')){
obj.container4 = 'found'
return obj
} else {
// let {nextLevel} = obj /no good
// return goingDeep(nextLevel) /no good
let nextLevel = Object.values(obj)
return goingDeep(...nextLevel)
}
}
originally had the destructuring at the parameter goingDeep = ({obj}) which I have used successfully for arrays so clearly I'm misunderstanding something(s) fundamental to destructuring objects

You are, indeed, misunderstanding how destructing works. If you have an object, destructing lets you create new variables with the same names as the properties in an object.
let obj = {
prop1: 'value1',
prop2: 'value2',
};
let {prop1, prop2, prop3} = obj;
console.log(prop1, prop2, prop3)

let {nextLevel} = obj
Is the same as
let nextLevel = obj.nextLevel;
I think you might be misunderstanding what destructing does.
To be recursive you will need to dynamically search all properties.
const goingDeep = obj => Object.getOwnPropertyNames(obj).reduce(
(prop, result) => result === null ? (obj[prop] === 'core' ? obj : goingDeep(obj[prop])) : result, null
);

Related

Convert an arbitrarily deeply nested value into an object key using Ramda or vanillaJS

I am using a library that returns a series of values in a slightly unconventional way. Rather than returning an array the library returns nested objects with the final leaf node included as a value. For example:
red.walk.fast.true becomes {red: {walk: {fast: 'true'}}}
or
climbing.ascending becomes {climbing: 'ascending'}
The format of nested objects actually suits my use case but I need a way of converting the final value into another nested object (with a null value - but this is not important) rather than a value. So:
{red: {walk: {fast: 'true'}}} becomes {red: {walk: {fast: {true: null}}}}
The formula needs to work for any arbitrlily deeply nested object. I am struggling to get a recursive function working with Ramda though. I thought the following would work:
const stringPropToObject = R.ifElse(
R.is(String),
R.assoc(R.__, null, {}),
mapAllStringPropsToObjs);
const mapAllStringPropsToObjs = R.map(stringPropToObject)
const updatedObject = mapAllStringPropsToObjs({ a: { b: { c: 'd' } } })
console.log(updatedObject);
mapAllStringPropsToObjs (attempts) to map over the objects properties passing them to stringPropToObject. This then looks at the property passed in.
If it is a string then it returns a new object with a property with
that strings name.
If it is an object it (should) recursively call
mapAllStringPropsToObjs passing the object untill a String value is
hit.
This results in "Cannot access 'mapAllStringPropsToObjs' before initialization". I understand why I am getting this error with the current order of declarations but I don't see how I can order them to avoid it - they are co-dependant functions.
Anyone know what I could do using Ramda or vanilla JS to be able to convert:
{climbing: 'ascending'} into {climbing: {ascending: null}} and
{red: {walk: {fast: 'true'}}} into {red: {walk: {fast: {true: null}}}}
or any other arbitrarily nested value.
Thanks in advance for any suggestions.
The simplest way to fix your co-dependent functions issue is to simply replace the reference in the first one to the second with a lambda:
const stringPropToObject = ifElse(
is(String),
assoc(__, null, {}),
x => mapAllStringPropsToObjs(x)
);
const mapAllStringPropsToObjs = map(stringPropToObject)
const updatedObject = mapAllStringPropsToObjs({ a: { b: { c: 'd' } } })
console.log (updatedObject)
<script src="//cdnjs.cloudflare.com/ajax/libs/ramda/0.27.0/ramda.js"></script>
<script> const {ifElse, is, assoc, __, map} = R </script>
Ramda isn't particularly designed to work well with recursion, but this is of course a basic issue in the language.
I would probably have written this differently, in vanilla JS, with something like this:
const transform = (obj) =>
Object .entries (obj)
.reduce ((a, [k, v]) => ({
...a,
[k]: typeof v == 'string' ? {[v]: null} : transform (v)
}), {})
But I think that your recursive Ramda solution with this minor tweak is simpler and cleaner.
You could iterate the entries and if no object, assign a new one.
function f(object) {
Object.entries(object).forEach(([k, v]) => {
if (v && typeof v === 'object') f(v);
else object[k] = { [v]: null };
});
}
var object = { red: { walk: { fast: 'true' } } };
f(object);
console.log(object);
As stated by #ScottSauyet you can use a function (anonymous or arrow) to call mapAllStringPropsToObjs() in the future when it's actually defined.
A piece of code that performs a delayed computation is called a thunk. According to wikipedia:
In computer programming, a thunk is a subroutine used to inject an
additional calculation into another subroutine. Thunks are primarily
used to delay a calculation until its result is needed, or to insert
operations at the beginning or end of the other subroutine.
In addition, I would also replace R.assoc with a flipped R.objOf to generate the object.
const objOfNull = flip(objOf)(null);
const mapAllStringPropsToObjs = map(ifElse(
is(String),
objOfNull,
x => mapAllStringPropsToObjs(x)
));
const updatedObject = mapAllStringPropsToObjs({ a: { b: { c: 'd' } } })
console.log (updatedObject)
<script src="//cdnjs.cloudflare.com/ajax/libs/ramda/0.27.0/ramda.js"></script>
<script> const {map, ifElse, is, flip, objOf} = R </script>
This solution is recursive and creates a new converted object instead of altering the old one.
function transformObject(obj, final = {}, curr = final) {
Object.entries(obj).forEach(([key, value]) => {
curr[key] = {}
if (typeof value !== 'object') curr[key][value] = null
else transformObject(value, final, curr[key])
})
return final
}
const dataOne = {climbing: {run: 'slow', hike: {mountain: 'fast'}}}
const dataTwo = {red: {walk: {fast: 'true'}}}
console.log(dataOne, transformObject(dataOne))
console.log(dataTwo, transformObject(dataTwo))

ES6: destructuring an object with symbols as keys

I have an object that contains symbols as keys. How do I do destructuring assignment in this case?
let symbol = Symbol()
let obj = {[symbol]: ''}
let { /* how do I create a variable here, that holds the value of [symbol] property? */ } = obj
I need to know if this possible, I do know the obvious and simple workarounds, but that's not what I am asking.
UPD. Funny enough I knew how to do that but typescript produced errors, and I thought I did something wrong in JS. Here's a fix for typescript users.
Use an alias (see assigning to new variable names):
let symbol = Symbol()
let obj = { [symbol] : 'value'}
let { [symbol]: alias } = obj
console.log(alias)
Use the same syntax for destructuring as for building the object:
let symbol = Symbol()
let obj = {[symbol]: 'foo'}
let { [symbol]: myValue } = obj;
console.log(myValue);

Read variable number of properties from object

Say I have this function signature:
export const readVariableProps = function(obj: Object, props: Array<string>) : any {
// props => ['a','b','c']
return obj['a']['b']['c'];
}
obviously, props is a variable length array, with an unknown list or properties to read from the given object.
is the only way to get this kind of dynamic behavior to use eval()?
How can I do this?
To get the equivalent of return obj['a']['b']['c']; where 'a', 'b' and 'c' are the values in the array like you show in your question, you can do something like this (you may have to convert some details to typeScript):
export const readVariableProps = function(obj: Object, props: Array<string>) : any {
return props.reduce(function(prior, next) {
return prior[next];
}, obj);
}
FYI, this type of scenario is exactly what .reduce() was designed for -
accumulating a value that is built by visiting every item in an array.
This will throw if anything other than the last property in the array does not exist.
You can use a for..of loop to set a variable to each nested property
const props = ['a','b','c'];
const obj = {a:{b:{c:123}}};
let res;
for (let prop of props) res = !res ? obj[prop] : res[prop];
console.log(res);

Assigning nested values in (partially) undefined objects

Say I want to assign a value like this:
x.label1.label2.label3 = someValue;
// or equivalently:
x['label1']['label2']['label3'] = someValue;
This works as long as x.label1.label2 is defined but runs into reference errors otherwise. Which makes sense of course. But is there an easy way to assign this anyway where it simply creates the necessary nested objects?
So for example, if x equals { label1: {}, otherLabel: 'otherValue' } I want to update x to become { label1: { label2: { label3: someValue } }, otherLabel: otherValue }
I think I might be able to write a function myself, but is there a language feature or standard library function that does this?
is there a language feature or standard library function that does this
No. You have to write your own function or use a library that provides such functionality.
Related: How to set object property (of object property of..) given its string name in JavaScript?
This is partially possible using the Proxy class. You can wrap your object in a Proxy and override the get trap to create another copy of the same proxy when you access a nonexistent property. This lets you recursively create "deep" properties. An example:
let traps = {
get: function (target, name) {
if (!(name in target))
target[name] = new Proxy({}, traps);
return target[name];
}
};
let x = new Proxy({}, traps);
Then you would use x like any object, except it has this special behavior:
x.label1.label2.label3 = 'foo';
which creates a nested hierarchy of objects. However, note that this will create an object even if you access a nonexistent property. Thus, you will have to use the in keyword to check if it really contains a given property.
I think you should indeed use a custom function such as:
function assignByPath(obj, path, value) {
var field = path.split('>'),
last = field.pop();
field.reduce(
function(node, f) {
return node[f] = node[f] instanceof Object ? node[f] : {};
}, obj
)[last] = value;
}
var myObj = {};
assignByPath(myObj, 'label1>label2>label3', 'someValue');
console.log(myObj);
Theoretically, you could also override Object.prototype, which would allow you to do:
myObj.assignByPath('label1>label2>label3', 'someValue');
But I would not recommend that.
You can use Array.prototype.shift(), Object.assign(), recursion
var x = {
label1: {},
otherLabel: "otherValue"
};
var nestprops = (props, value, obj, o, curr = props.shift()) => props.length
? nestprops(props, value, (Object.assign(obj, {[curr]: {}}) && obj[curr]), o)
: ((!value || value) && (obj[curr] = value) && o);
console.log(nestprops(["label1", "label2", "label3"], "someValue", x, x));
Check length of keys inside label1 object if its equal to 0 then modify it to your desired object.
Here is a snippet, hope it helps.
var obj = { label1: {}, otherLabel: 'otherValue' };
if(Object.keys(obj.label1).length == 0 ) {
obj.label1 = { label2: { label3: "value3" } };
}
console.log(obj);

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. :(

Categories