This question already has answers here:
How do I correctly clone a JavaScript object?
(81 answers)
Closed 2 years ago.
Problem: I have a prop in a Vue component.
props: {
user_info: {
type: Object,
default() {
return {}
}
}
}
I need to use this prop not only for read, but also to make some changes in it. But i cant change the prop, so i cloned it to a data variable
mounted() {
this.clonedUserInfo = {...this.user_info}
}
clonedUserInfo is an empty object in Data option.
But when i change clonedUserInfo, my prop user_info also changes.
So is there any way to brake this Data dependency from Props?
By the way if i am cloning not a Props but a Vuex state variables - it clones fine without changing the base State variable.
Yes there is a way.
mounted() {
this.clonedUserInfo = JSON.parse(JSON.stringify(this.user_info))
}
For now you are copying by reference. And thanks to my answer you will be copying by value.
Look at this SO answer to understand the difference : https://stackoverflow.com/a/430958/3956205
You are shallow copying user_info.
If you only have value only vars in user_info this will work, otherwise you'll update the original values.
You need to deep copy user_info.
LODASH
You can solve this using Lodash's CloneDeep.
npm i --save lodash.clonedeep
const cloneDeep = require('lodash.clonedeep');
NPM: https://www.npmjs.com/package/lodash.clonedeep
Docs: https://lodash.com/docs/4.17.15#cloneDeep
USE A FUNCTION
You can try this to deep clone objects:
function cloneDeepWithoutLodash(src) {
let target = {};
for (const prop in src) {
if (src.hasOwnProperty(prop)) {
if(src[prop] != null && typeof src[prop] === 'object') {
target[prop] = cloneDeepWithoutLodash(src[prop]);
} else {
target[prop] = src[prop];
}
}
}
return target;
}
const source = {a: { b:2 }, c:3};
const target = cloneDeepWithoutLodash(source);
source.a.b = 100; // Lets change the source to check if target changes
console.log(source.a.b); // 100
console.log(target.a.b); // 2
IMPORTANT
Always try to avoid JSON.parse(JSON.stringify(yourObject)).
By doing this you will lose any Javascript property that has no equivalent type in JSON, like Function or Infinity. Any property that’s assigned to undefined will be ignored by JSON.stringify, causing them to be missed on the cloned object.
Also, some objects are converted to strings, like Date objects for example (also, not taking into account the timezone and defaulting to UTC), Set, Map and many others.
You are creating a new object clonedUserInfo that is correct. But all objects inside of clonedUserInfo will be copied by reference.
Try smth like cloneDeep perhaps
https://lodash.com/docs/4.17.15#cloneDeep
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()
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.
Every time I try to access a deep level property (I guess an item in an array through it's index) I get the following error:
ERROR TypeError: Cannot assign to read only property 'value' of object '[object Object]'
Find below the code I'm trying to get running, but will throw the error above:
#Input() data: CivilLiabilityQuestionnaireModel;
public questions: CivilLiabilityQuestionnaireModel;
this.questions = { ...this.data };
const questionGroupIndex = 0;
const questionGroupItemIndex = 0;
this.questions
.questionGroup[questionGroupIndex]
.questionGroupItems[questionGroupItemIndex]
.answers[0]
.value = form[val];
A little more details of what's possible:
// This works
this.questions.id = 'hotdog';
// This doesn't work
// ERROR TypeError: Cannot assign to read only property 'id' of object '[object Object]'
this.questions.questionGroup[questionGroupIndex].id = 'hamburger';
I thought this would only be possible with using the spread operator, but this will turn all my arrays into objects, while I need to keep my arrays at any cost.
Here the spread solution I tried:
this.questions = {
...this.questions,
questionGroup: {
...this.questions.questionGroup,
[questionGroupIndex]: {
...this.questions.questionGroup[questionGroupIndex],
questionGroupItems: {
...this.questions.questionGroup[questionGroupIndex].questionGroupItems,
[questionGroupItemIndex]: {
...this.questions.questionGroup[questionGroupIndex].questionGroupItems[questionGroupItemIndex],
answers: {
...this.questions.questionGroup[questionGroupIndex].questionGroupItems[questionGroupItemIndex].answers,
[0]: {
...this.questions.questionGroup[questionGroupIndex].questionGroupItems[questionGroupItemIndex].answers[0],
value: form[val]
}
}
}
}
}
}
};
I managed to find the solution to the problem.
In my example from the original question I did the following:
this.questions = { ...this.data };
The solution came from another post: https://stackoverflow.com/a/40922716/2664414
The issue with the example above is that you only make a clone of the root property, while the nested child properties remain frozen.
Since my value came from the NGRX store, it's basically in a "freeze" state and can't be adjusted.
To fix this, you can use cloneDeep() from lodash to make sure the nested properties get cloned aswell and lose the "freeze" state.
this.questions = _.cloneDeep(this.data);
Maybe you defined something somewhere as constant and you're trying to change it. Javascript does not support read only props naturally so basically in practice the only reason to get this error is that you either defined some variable/prop as constant or you defined a obj's prop as writable=false and you're trying to change it. Look for questionGroupIndex or questionGroupItemIndex you might be trying to change them. Either way the problem is not in the nesting.
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
}
I've been using dean edwards base.js (http://dean.edwards.name/weblog/2006/03/base/) to organise my program into objects ( base.js is amazing btw, if you havent used it before !).Anyway, my question is generic and you don't have to know base.js to know my answer.
I have a property in one of my objects called ref which is a reference to a DOM element, and this object is meant to be saved as JSON using JSON.stringify, but as you can imagine since DOM elements are circular structure, I won't be able to convert the object into JSON.
Now to get around this problem I have a method called html() which is meant to return the ref property, but I need to have ref as a private property which is only accessible from within the object, and hence won't be sent to stringify.
What's the best way to do that?
You probably know that you cannot have private properties in JavaScript.
Interestingly, if you pass an object to JSON.stringify which has a method toJSON, JSON.stringify will automatically call that method to get a JSONable representation of that object. So all you have to do is implement this method.
For example you can create a shallow copy of the object which only contains the properties you want to copy:
MyConstructor.prototype.toJSON = function() {
var copy = {},
exclude = {ref: 1};
for (var prop in this) {
if (!exclude[prop]) {
copy[prop] = this[prop];
}
}
return copy;
};
DEMO
Another way would be to use a custom replacer function, but it might be more difficult to control which ref to exclude and which one to keep (if different objects have ref properties):
JSON.stringify(someInstance, function(key, value) {
if(key !== 'ref') {
return value;
}
});
DEMO
here is sample to to set variable visibility
function Obj(){
this.ref = 'public property'; // this property is public from within the object
var ref = 'private proerty'; // this property is private.
var self = this;
this.showRef = function(){
alert(ref);
alert(self.ref);
};
}
var obj = new Obj();
obj.showRef();