I have this small function (within my Angular 7 application) which uses JavaScript reduce(), and locates an object within a nested array of objects. I can then proceed to update certain properties on the fly.
Now, in addition to this find logic, I would like to also insert/delete an object into/from the nested array.
Question is: once I do locate my object, can I push() and/or delete an object ?
const input={UID:2,GUID:"",LocationName:"USA",ParentLocation:null,subs:[{UID:42,GUID:"",LocationName:"New Jersey",Description:"",subs:[{UID:3,GUID:"",LocationName:"Essex County",ParentLocation:null,"subs":[{UID:4,LocationName:"Newark",ParentLocation:3,"subs":[{"UID":49,"GUID":"","LocationName":"Doctor Smith's Office","LocationType":{"UID":2,"LocationTypeName":"Practice","Description":"other location"},"subs":[{"HostID":38,"HostName":"Ocean Host",}]}]}]}]}]};
const findUIDObj = (uid, parent) => {
const { UID, subs } = parent;
if (UID === uid) {
const { subs, ...rest } = parent;
return rest;
}
if (subs) return subs.reduce((found, child) => found || findUIDObj(uid, child), null);
};
console.log(findUIDObj(49, input));
var obj = findUIDObj(49, input);
delete obj;
For example, in my Angular 7 app, it complains if I attempt to delete the found object:
ex/
var obj = findUIDObj(49, input);
delete obj;
'delete' cannot be called on an identifier in strict mode.
Looking briefly at your code, I see you are using a const identifier to declare your data collection. We only use const for static data that does not change, and that’s the purpose of it. So, first of all matters, that seems to be the problem. To test it change it to let. Now, as for methods for data management, immutability is worthy of your consideration for many reasons, but namely Angular will rerender the entire object regardless of altering the existing object, or receiveing a new object. You can look up Immutable JavaScript to understand more. In many cases, creating immutable data management is done with a library, you can do it yourself. Basically, create a function called copy( data ), or something so that you pass in the original object, but you get a copy of it in return with no reference to the original object. That way one does not accidentally change the original object. To do this you can do this inside of your copy function: return JSON.parse(JSON.stringify( data )) ;
The only problem you might run into here is deep nested objects, or objects with circular references can cause problems. I have an overriding stringify method to mange this in little libraries I’ve written.
delete obj would never do what you want: first of all, it is not even an object from your input, since the function created a new object from the found object, excluding the subs property, and returned that. But more importantly, delete is used for deleting properties, not objects.
It seems you want to remove a matching object from its parent subs property. For that you would need to mutate the subs array, so it would exclude the matching object. For that to work in a generic way, your input should be an array. Otherwise that root object could not be removed from anything.
With that in mind, your lookup function should return the array in which the match was found and at which index. With those pieces of information you can decide to remove that element from the array, or to insert another object at that index.
Here is how it could work with removal:
const input=[{UID:2,GUID:"",LocationName:"USA",ParentLocation:null,subs:[{UID:42,GUID:"",LocationName:"New Jersey",Description:"",subs:[{UID:3,GUID:"",LocationName:"Essex County",ParentLocation:null,"subs":[{UID:4,LocationName:"Newark",ParentLocation:3,"subs":[{"UID":49,"GUID":"","LocationName":"Doctor Smith's Office","LocationType":{"UID":2,"LocationTypeName":"Practice","Description":"other location"},"subs":[{"HostID":38,"HostName":"Ocean Host",}]}]}]}]}]}];
const findUIDObj = (uid, arr) => {
if (!arr) return;
const idx = arr.findIndex(obj => obj.UID === uid);
if (idx > -1) return [arr, idx];
for (const obj of arr) {
const result = findUIDObj(uid, obj.subs);
if (result) return result;
}
};
console.log(findUIDObj(49, input));
const [arr, idx] = findUIDObj(49, input) || [];
if (arr) {
arr.splice(idx, 1); // Remove object from its parent array
}
Related
I am using the hookstate.js library and am trying to something as simple as to get the entire Javascript object from the State object without having to reference the key of the object.
For example, state.set({first:'John',last:'Doe'}) and then somehow access the entire object (as a bona fide JS object, not State) without having to know the object key and reference state.first.get().
Is there no built in way to do such a simple thing?
I can do so with a reduce:
const { keys } = state
const jsObject= keys.reduce((pre, cur) => {
const stateVal = state[cur].get()
return { ...pre, [cur]: stateVal }
}, {})
However, this is not possible if I am expecting nested objects.
Author of Hookstate here. You can call State.get() on the root of the state and use noproxy and stealth options (since Hookstate 4 version) for the get method. It will do what you require.
Did you try calling get() on the root of the object?
Like state.get()
I need to reserialize an object from a WebWorker sharing the same definitions.
Upon reception of the message, I'm losing all the prototype functions.
Doing
worker.onmessage = ({ data }) => {
this.programParser = Object.assign(new ProgramModel(), data);
}
Works only for first level prototype functions, but I need a solution for all the nested classes (this object has a large inheritance and dependency tree).
What could I do to do that ?
Current data model looks like :
(note the Object prototypes)
Using flatted and custom revive function (I added the class name in each object from the serializing function), I can achieve a much closer model to what I would need, but some nested references aren't treated as original class objects.
The flatted revive function is the following :
const model = parse(data, (key, val) => {
if (val !== null && val.className && val.className in models) {
if (val.className === "DataProvider") {
console.log(val)
return new DataProvider(val.providerData)
}
return Object.assign(new (<any>models)[val.className](), val)
}
return val;
})
The flatted library is used to keep clear of circular issues of JSON serialization : https://github.com/WebReflection/flatted
Minimal exemple of the issue : https://codesandbox.io/s/zealous-https-n314w?file=/src/index.ts (Object is lost after 2nd level referencing)
I don't want to take any credit from this previous answer, but while the explanation/process through is nice and sound, I believe the proposed solution is not the best one, and btw, I am the author of the flatted library that actually explained, and helped out, in this issue filed against such library, without even knowing there was a discussion here ... sorry I am late ...
The missing piece of the previous answer is that the same instance gets updated over and over while reviving the whole structure, but nothing like that is actually needed, because either Set or WeakSet can help speeding up the process, avoiding upgrading what's been upgraded already, over and over.
const {setPrototypeOf} = Reflect;
const upgraded = new Set;
const ret = parse(str, (_, v) => {
if (v && v.className && models[v.className] && !upgraded.has(v)) {
upgraded.add(v);
setPrototypeOf(v, models[v.className].prototype);
}
return v;
});
This change doesn't strictly improve the reason it either works or it is the best solution, compared to substitution, but it takes into account performance and redundant upgrades, 'cause unnecessary setPrototypeOf calls, might not be desired at all 😉
This is a classical problem with tying the knot on a circular structure. When deserialising, the flatted library has to start somewhere, passing in the original not-yet-revived object as the argument. It could in theory pass a proxy (or an object with getters) that parse the involved objects on-demand in the right order, but if all objects in the circle need to be revived this would lead to a stack overflow, as JS doesn't use lazy evaluation where you can reference the result of a call before having evaluated it.
A pure approach would actually need a reviver function that does support such a lazy approach, not accessing the object passed to it until after the deserialisation has finished:
const cache = new WeakMap();
const ret = parse(str, (k, v) => {
if (cache.has(v)) return cache.get(v);
if (v && v.className && (<any>models)[v.className]) {
const instance = new (<any>models)[v.className]();
cache.set(v, instance);
for (const p in instance) {
Object.defineProperty(instance, p, {
set(x) { v[p] = x; }, // not necessary if your models were immutable
get() { return v[p]; },
enumerable: true,
});
}
return instance;
}
return v;
});
This essentially makes the instance a proxy over v, getting all its values from there. When flatted does tie the knot by assigning revived values to properties of v, they'll also become available on instance. Apart from the .className, no properties of v are accessed during the reviver call. Only when you access ret.something, the object properties will be read, and will contain the revived objects by then.
The downside of this approach is that a) all models will need to declare and initialise their properties upfront (which your B for example doesn't do) so that the for (const p in instance) loop works, and b) all your model properties will be replaced with accessors, which is inefficient and may conflict with the internal implementation.
An alternative is to forward the property assignments that flatted does on the original object to the newly constructed instance, even if they happen after the reviver call:
const cache = new WeakMap();
const ret = parse(str, (k, v) => {
if (cache.has(v)) return cache.get(v);
if (v && v.className && (<any>models)[v.className]) {
const instance = new (<any>models)[v.className]();
cache.set(v, instance);
Object.assign(instance, v);
for (const p in v) {
Object.defineProperty(v, p, {
set(x) { instance[p] = x; },
get() { return instance[p]; }, // not truly necessary but helps when `v` is logged
});
}
return instance;
}
return v;
});
This basically reflects how you originally constructed the circular reference using a.setB(new B(a)), except by directly assigning the property instead of using the model's setB method, which the reviver doesn't know about. When using this approach, make sure to explicitly document that all your models must support constructor calls without arguments, and direct property assignment (be it via Object.assign or instance[p] = x). If setters are necessary, use accessor properties (set syntax) instead of set…() methods.
The simplest and probably fastest approach would be to not return a new object from the reviver at all, but keep its identity:
const ret = parse(str, (k, v) => {
if (v && v.className) {
const model = (<any>models)[v.className];
if (model && Object.getPrototypeOf(v) != model.prototype) {
Object.setPrototypeOf(v, model.prototype);
}
}
return v;
});
This restores the prototype methods by simply swapping out the prototype of the revived object for the expected one. However, this means that your models are instantiated without a constructor call, they may not use enumerable getters, and they cannot maintain private state even if they expose it through .toJSON correctly.
In my component i have declarated some data like this:
data() {
return {
defaultValue: {json object with some structure},
activeValue: {}
...
And in component methods a make copy this value:
this.activeValue = this.defaultValue
But problem is, after change this.activeValue value a have changes in this.defaultValue too.
If i use Object.freeze(this.defaultValue) and trying change this.activeValue i have get error - object is not writable.
How i can make copy of data but without reference?
If you have simple object, quickest and easiest way is to just use JSON.parse and JSON.stringify;
const obj = {};
const objNoReference = JSON.parse(JSON.stringify(obj));
this.activeValue = { ...this.defaultValue }
Using an ES6 spread operator will help you to do a copy if you do not have a nested object. If you equate using equal = sign, it will not create a new object, it will just create a variable with the reference to the current object (like a shallow copy).
To do a complete deep copy, even it is nested object, go for this:
const objNoReference = JSON.parse(JSON.stringify(obj));
as suggested by Owl.
Click to read more for better understanding of the concept
A nicer way rather than using JSON.parse, JSON.stringify is:
this.activeValue = {...this.defaultValue}
but this is not natively supported by some browser (IE), unless used with a transpiler (babel)
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Spread_syntax
Update
Considering your originial question is about a way in Vue, there is also a native method in vue:
this.activeValue = Vue.util.extend({}, this.defaultValue)
as for this answer.
Hope this helps!
Objects are assigned and copied by reference.
All operations via copied references (like adding/removing properties) are performed on the same single object.
To make a “real copy” (a clone) we can use Object.assign for the so-called “shallow copy” (nested objects are copied by reference).
For “deep cloning” use _.cloneDeep(obj) from loadash library.
JSON stringify&parse method have some issues like converting date objects to strings. It also cannot handle special data types like Map,Set,function etc... This is prone to future bugs.
I use the following method to deep copy an object.
REMEMBER! this is not a complete application of cloning. There are more data types to handle like Blob, RegExp etc...
const deepClone = (inObject) => {
let outObject, value, key
if (typeof inObject !== "object" || inObject === null)
return inObject
if (inObject instanceof Map) {
outObject = new Map(inObject);
for ([key, value] of outObject)
outObject.set(key, deepClone(value))
} else if (inObject instanceof Set) {
outObject = new Set();
for (value of inObject)
outObject.add(deepClone(value))
} else if (inObject instanceof Date) {
outObject = new Date(+inObject)
} else {
outObject = Array.isArray(inObject) ? [] : {}
for (key in inObject) {
value = inObject[key]
outObject[key] = deepClone(value)
}
}
return outObject
}
You can use 'JSON.parse and stringify' or using some clone function in the libs like lodash (underscore, ramda...)
Also a simple solution is to store defaultValue: {json object with some structure} with JSON.stringify(defaultValue) in a string variable:
var x = JSON.stringify(this.defaultValue);
If you need it as JSON object again you can get it with JSON.parse():
var newObject = JSON.parse(x);
The object reference is also broken doing it this way, x will stay unchanged if the content of the object defaultValue is altered.
I am puzzled why this following bit of code will return mutations of both the local and global array:
var globalarray = [1,2,3];
function test(){
let localarray = globalarray;
localarray.push(4);
console.log(localarray);
console.log(globalarray);
}
setInterval(test, 2000);
Returns:
[1,2,3,4] for both
My impression was that localarray would be a copy of globalarray. I saw another answer that said in order to make a copy of an array you need to use .slice().reverse(), which seems like a workaround. Why does it not just create a new local copy? Is there a simple and efficient way to make a local copy of a global array? Otherwise it seems like making multiple mutations to a global array is terrible for performance.
The reason for this in your code is because you are simply telling your test function to point to the globalarray with the = operator. This is because in JavaScript, variable assignments do not inherently "copy" objects into the new variables; this might seem confusing, so just think of the = operator as a sign that points your code to the location of an object.
The only times that the = operator is making new copies is when you are working with primitive types. In those cases, you cannot inherently change what those objects are, so = is sufficient to make a copy like you would expect.
The reason for the .slice().reverse() is to work around the problem you are seeing. Another way you could do this is by using let localarray = globalarray.map(e => e), or as samee suggested, by using let localarray = [...globalarray]. The .map operator takes the function given to it as the first argument and applies it to every element, and then it stores the result in a different array at another location that is not the same as globalarray.
Keep in mind that these methods, and any others that might be suggested to you, are shorthand ways of doing
let localarray = new Array(globalarray.length);
for (let i = 0; i < globalarray.length; i++) {
localarray[i] = globalarray[i];
}
// localarray can now be freely modified because it does not point to the same array as globalarray
Also keep in mind that if you need to also create copies of the elements inside of the arrays, then you will have to use more comprehensive copying code. There are libraries that can do this sort of heavy-duty copying if you really need it.
In JavaScript (as in many other languages), objects are passed by reference. Arrays are also passed by reference (because an array is actually a type of object). So when you say: let localarrray = globalarray, you are actually setting localarray to a pointer that resolves to globalarray.
There are several strategies for obtaining a true copy. If you're trying to get a fresh copy of the original array, the Array prototype function of .map() is one of the most targeted tools for the job. It would look like this:
let localarray = globalarray.map(element => element);
Simple way to clone an array is just
let localarray = globalarray.slice();
I do it a different way to deep cloning:
clone: function() {
var clone = undefined;
var instance = this;
if ( XScript.is.xobject(instance) ) {
clone = {};
for ( var prop in instance ) {
if ( instance.hasOwnProperty(prop) ) {
var p = instance[prop];
if ( XScript.is.xobject(p) ) p = p.clone();
clone[prop] = p;
}//END IF this
}//END FOR prop
return clone;
}//END IF xobject
if ( XScript.is.xarray(instance) ) {
clone = instance.slice(0);
return clone;
}
return clone;
}//END FUNCTION clone
This clone will require you attaching the clone object to the object prototype and check to see if its an array, object, or other. I am not changing the function to fit all your needs because one should learn to somewhat how to change it and what to do instead of copy pasta. Plus it is just an example.
I want to map an array, returning the same array except .created property, that I'm transforming from milliseconds to Date Object.
const posts = data.map((post) => {
post.created = new Date(post.created); //milliseconds to Date Object
return post;
})
This function do the result that I expected, but I don't like because the function inside is not pure, is mutating the object. How can be an alternative to do the same with pure function?
Thank you so much.
You can achieve this using Object.assign (docs) inside the map:
const posts = data.map((post) => {
return Object.assign({}, post, { created: new Date(post.created) });
})
This essentially clones the original post and then overrides the created property.
I would provide both solutions.
Clone the array and its objects
data.map(post => Object.assign({created: new Date(post.created)},post));
The first argument of Object.assign is the target object. It's also the object that Object.assign returns.
Modify the existing array
for(let post of data){
post.created = new Date(post.created);
}
This method is, of course, faster and costs less resources since it doesn't need to initialize a new array and its objects then copy all objects' properties like the other does.
The choice is yours. Just make sure you know what you're doing.
You can make copy without copying all of the attributes from the old object:
const posts = data.map((post) => {
const obj = Object.create(post);
obj.created = new Date(post.created); // shadow created with the new value
return obj;
});
In practice it uses JavaScript inheritance scheme so that the new object only has created as it's own attribute and the rest is inherited from the original object.