Cloning an array in Javascript / Typescript
I have an array of two objects:
genericItems: Item[] = [];
backupData: Item[] = [];
I am filling my HTML table with data genericItems
. The table is modifiable. There is a reset button to undo all changes made with backUpData
. This array is populated by the service:
getGenericItems(selected: Item) {
this.itemService.getGenericItems(selected).subscribe(
result => {
this.genericItems = result;
});
this.backupData = this.genericItems.slice();
}
My idea was that the user changes would be reflected in the first array and the second array could be used as a backup for the reset operation. The problem I'm running into here is that the user is modifying the table ( genericItems[])
, the second array is backupData
also changing).
How does this happen and how to prevent it?
source to share
Cloning arrays and objects in JavaScript has a different syntax . Sooner or later, everyone learns the difference and ends up here.
In Typescript and ES6, you can use the spread operator for array and object:
const myClonedArray = [...myArray]; // This is ok for [1,2,'test','bla']
// But wont work for [{a:1}, {b:2}].
// A bug will occur when you
// modify the clone and you expect the
// original not to be modified.
// The solution is to do a deep copy
// when you are cloning an array of objects.
To make a deep copy of an object, you need an external library:
import * as cloneDeep from 'lodash/cloneDeep';
const myClonedArray = cloneDeep(myArray); // This works for [{a:1}, {b:2}]
The spread operator also works on an object, but it will only make a shallow copy (first layer of children)
const myShallowClonedObject = {...myObject}; // Will do a shallow copy
// and cause you an un expected bug.
To make a deep copy of an object, you need an external library:
import * as cloneDeep from 'lodash/cloneDeep';
const deeplyClonedObject = cloneDeep(myObject); // This works for [{a:{b:2}}]
source to share
Using a map or other similar solution will not help you deeply clone an array of objects. An easier way to do this without adding a new library is to use JSON.stringfy and then JSON.parse.
In your case, this should work:
this.backupData = JSON.parse(JSON.stringify(genericItems));
- Using the latest JS / TS, installing a large library like lodash for one or two functions is a bad move. You will appreciate the performance of your application, and eventually you will have to maintain library updates! check out https://bundlephobia.com/ result?p=lodash@4.17.15
For small objects cloneDeep may be faster, but for larger and deeper objects the json clone becomes faster. So in this case, you shouldn't hesitate to use it. check https://www.measurethat.net/Benchmarks/Show/6039/0/lodash-clonedeep-vs-json-clone-larger-object and info https://v8.dev/blog/cost-of-javascript- 2019 # json
It's inconvenient that your original object needs to be converted to JSON.
source to share
The next line of your code creates a new array, copies all object references from genericItems
into this new array, and assigns it backupData
:
this.backupData = this.genericItems.slice();
So, while backupData
and genericItems
are different arrays, they contain the same exact object references.
You can bring a deep copy library for you (as @LatinWarrior mentioned).
But if Item
not too complicated, maybe you can add a method to it clone
to deep clone an object:
class Item {
somePrimitiveType: string;
someRefType: any = { someProperty: 0 };
clone(): Item {
let clone = new Item();
// Assignment will copy primitive types
clone.somePrimitiveType = this.somePrimitiveType;
// Explicitly deep copy the reference types
clone.someRefType = {
someProperty: this.someRefType.someProperty
};
return clone;
}
}
Then call clone()
for each element:
this.backupData = this.genericItems.map(item => item.clone());
source to share
Below code can help you to copy first level objects
let original = [{ a: 1 }, {b:1}]
const copy = [ ...original ].map(item=>({...item}))
, so for the case below, the values ββremain unchanged
copy[0].a = 23
console.log(original[0].a) //logs 1 -- value didn't change voila :)
Crash for this case
let original = [{ a: {b:2} }, {b:1}]
const copy = [ ...original ].map(item=>({...item}))
copy[0].a.b = 23;
console.log(original[0].a) //logs 23 -- lost the original one :(
Final tip:
I would say go for the lodash cloneDeep
API, which helps you copy objects within objects by completely dereferencing them from the original. It can be installed as a separate module.
Refer to the documentation: https://github.com/lodash/lodash
Custom package : https://www.npmjs.com/package/lodash.clonedeep
source to share
I have the same problem with the primeNg DataTable parameter. After trying and crying, I fixed the problem using this code.
private deepArrayCopy(arr: SelectItem[]): SelectItem[] {
const result: SelectItem[] = [];
if (!arr) {
return result;
}
const arrayLength = arr.length;
for (let i = 0; i <= arrayLength; i++) {
const item = arr[i];
if (item) {
result.push({ label: item.label, value: item.value });
}
}
return result;
}
To initialize the backup value
backupData = this.deepArrayCopy(genericItems);
To discard changes
genericItems = this.deepArrayCopy(backupData);
The magic bullet is to recreate the elements using {}
constructor calls instead. I've tried new SelectItem(item.label, item.value)
that doesn't work.
source to share
It looks like you got it wrong about where you are making a copy of the array. Take a look at my explanation below and a slight modification to the code that should work, helping you reset the data to a previous state.
In your example, I see the following:
- you are making a request to get shared items
- after you get the data you set the results to this.genericItems
- directly after that you set backupData as result
As I understand it, you do not want the 3rd item in this order?
Would it be better:
- you are making a data request
- create a backup of the current this.genericItems file
- then set genericItems as the result of your request
Try the following:
getGenericItems(selected: Item) {
this.itemService.getGenericItems(selected).subscribe(
result => {
// make a backup before you change the genericItems
this.backupData = this.genericItems.slice();
// now update genericItems with the results from your request
this.genericItems = result;
});
}
source to share
It sounds like you want a Deep Copy of the object. Why not use it Object.assign()
? No libaries needed and its a one-liner :)
getGenericItems(selected: Item) {
this.itemService.getGenericItems(selected).subscribe(
result => {
this.genericItems = result;
this.backupDate = Object.assign({}, result);
//this.backupdate WILL NOT share the same memory locations as this.genericItems
//modifying this.genericItems WILL NOT modify this.backupdate
});
}
More on Object.assign()
: https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Objects/Object/assign
source to share
If your elements in the array are not primitive, you can use the spread operator for that.
this.plansCopy = this.plans.map(obj => ({...obj}));
Full answer: fooobar.com/questions/14831200 / ...
source to share