Cloning an array in Javascript/Typescript
ArraysAngularTypescriptArrays Problem Overview
I have array of two objects:
genericItems: Item[] = [];
backupData: Item[] = [];
I am populating my HTML table with genericItems
data. The table is modifiable. There is a reset button to undo all changes done with backUpData
. This array is populated by a 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 will get reflected in first array and second array can be used as backup for reset operation. The issue I am facing here is when the user modifies the table (genericItems[])
the second array backupData
also gets modified.
How is this happening and how to prevent this?
Arrays Solutions
Solution 1 - Arrays
Clone an object:
const myClonedObject = Object.assign({}, myObject);
Clone an Array:
- Option 1 if you have an array of primitive types:
const myClonedArray = Object.assign([], myArray);
- Option 2 - if you have an array of objects:
const myArray= [{ a: 'a', b: 'b' }, { a: 'c', b: 'd' }];
const myClonedArray = [];
myArray.forEach(val => myClonedArray.push(Object.assign({}, val)));
Solution 2 - Arrays
Cloning Arrays and Objects in javascript have a different syntax. Sooner or later everyone learns the difference the hard way and end 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 do a deep copy of an object you need an external library:
import {cloneDeep} from 'lodash';
const myClonedArray = cloneDeep(myArray); // This works for [{a:1}, {b:2}]
The spread operator works on object as well but it will only do a shallow copy (first layer of children)
const myShallowClonedObject = {...myObject}; // Will do a shallow copy
// and cause you an un expected bug.
To do a deep copy of an object you need an external library:
import {cloneDeep} from 'lodash';
const deeplyClonedObject = cloneDeep(myObject); // This works for [{a:{b:2}}]
Solution 3 - Arrays
Using map or other similar solution do not help to clone deeply an array of object. An easier way to do this without adding a new library is using JSON.stringfy and then JSON.parse.
In your case this should work :
this.backupData = JSON.parse(JSON.stringify(genericItems));
-
When using the last version of JS/TS, installing a large library like lodash for just one/two function is a bad move. You will heart your app performance and in the long run you will have to maintain the library upgrades! check https://bundlephobia.com/[email protected]
-
For small objet lodash cloneDeep can be faster but for larger/deeper object json clone become faster. So in this cases you should not hesitate to use it. check https://www.measurethat.net/Benchmarks/Show/6039/0/lodash-clonedeep-vs-json-clone-larger-object and for infos https://v8.dev/blog/cost-of-javascript-2019#json
-
The inconvenient is that your source object must be convertible to JSON.
Solution 4 - Arrays
try the following code:
this.cloneArray= [...this.OriginalArray]
Solution 5 - Arrays
The following line in your code creates a new array, copies all object references from genericItems
into that new array, and assigns it to backupData
:
this.backupData = this.genericItems.slice();
So while backupData
and genericItems
are different arrays, they contain the same exact object references.
You could bring in a library to do deep copying for you (as @LatinWarrior mentioned).
But if Item
is not too complex, maybe you can add a clone
method to it to deep clone the object yourself:
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()
on each item:
this.backupData = this.genericItems.map(item => item.clone());
Solution 6 - Arrays
Below code might help you to copy the first level objects
let original = [{ a: 1 }, {b:1}]
const copy = [ ...original ].map(item=>({...item}))
so for below case, values remains intact
copy[0].a = 23
console.log(original[0].a) //logs 1 -- value didn't change voila :)
Fails 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 advice:
I would say go for lodash cloneDeep
API ( This can be installed as a separate module ) which helps you to copy the objects inside objects completely dereferencing from original one's.
Refer documentation: https://github.com/lodash/lodash
Individual Package : https://www.npmjs.com/package/lodash.clonedeep
Solution 7 - Arrays
I have the same issue with primeNg DataTable. After trying and crying, I've fixed the issue by 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;
}
For initializing backup value
backupData = this.deepArrayCopy(genericItems);
For resetting changes
genericItems = this.deepArrayCopy(backupData);
The magic bullet is to recreate items by using {}
instead of calling constructor.
I've tried new SelectItem(item.label, item.value)
which doesn't work.
Solution 8 - Arrays
you can use map function
toArray= fromArray.map(x => x);
Solution 9 - Arrays
Clone an object / array (without reference) in a very powerful way
You can get deep-copy of your object
/ array
using @angular-devkit
.
import { deepCopy } from '@angular-devkit/core/src/utils/object';
export class AppComponent {
object = { .. some object data .. }
array = [ .. some list data .. ]
constructor() {
const newObject = deepCopy(this.object);
const newArray = deepCopy(this.array);
}
}
Solution 10 - Arrays
the easiest way to clone an array is
backUpData = genericItems.concat();
This will create a new memory for the array indexes
Solution 11 - Arrays
If your items in the array are not primitive you can use spread operator to do that.
this.plansCopy = this.plans.map(obj => ({...obj}));
Complete answer : https://stackoverflow.com/a/47776875/5775048
Solution 12 - Arrays
Try this:
[https://lodash.com/docs/4.17.4#clone][1]
var objects = [{ 'a': 1 }, { 'b': 2 }];
var shallow = _.clone(objects);
console.log(shallow[0] === objects[0]);
// => true
Solution 13 - Arrays
It looks like you may have made a mistake as to where you are doing the copy of an Array. Have a look at my explanation below and a slight modification to the code which should work in helping you reset the data to its previous state.
In your example i can see the following taking place:
- you are doing a request to get generic items
- after you get the data you set the results to the this.genericItems
- directly after that you set the backupData as the result
Am i right in thinking you don't want the 3rd point to happen in that order?
Would this be better:
- you do the data request
- make a backup copy of what is current in this.genericItems
- then set genericItems as the result of your request
Try this:
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;
});
}
Solution 14 - Arrays
Looks like what you want is Deep Copy of the object. Why not use 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
Solution 15 - Arrays
Try this
const returnedTarget = Object.assign(target, source);
and pass empty array to target
in case complex objects this way works for me
$.extend(true, [], originalArray)
in case of array
$.extend(true, {}, originalObject)
in case of object