How to keep an array with objects immutable in javascript? - javascript

I want to make an array based on two arrays - "ideaList" and "endorsements" declared globally. As ideaList and endorsements are used in other parts of the program I need them to be immutable, and I thought that .map and .filter would keep this immutability.
function prepareIdeaArray(){
var preFilteredIdeas=ideaList
.filter(hasIdeaPassedControl)
.map(obj => {obj.count = endorsements
.filter(x=>x.ideaNumber===obj.ideaNumber)
.reduce((sum, x)=>sum+x.count,0);
obj.like = endorsements
.filter(x=>x.ideaNumber===obj.ideaNumber && x.who===activeUser)
.reduce((sum, x)=>sum+x.count,0)===0?false:true
obj.position = generatePosition(obj.status)
obj.description = obj.description.replace(/\n/g, '<br>')
return obj;});
preFilteredIdeas.sort(compareOn.bind(null,'count',false)).sort(compareOn.bind(null,'position',true))
return preFilteredIdeas;
}
However, when I console.log ideaList after this function has been executed, I remark that objects of the array all have the "count", "like", "position" properties with values, which proves that the array has been mutated.
I tried by using .map only, but same result.
Would you know how I could prevent ideaList to get mutated? Also I would like to avoid to use const, as I declare ideaList globally first, and then assign to it some data in another function.

You're not mutating the array itself but rather the objects that the array contains references to. .map() creates a copy of the array but the references contained in it points to the exact same objects as the original, which you've mutated by adding properties directly to them.
You need to make copies of these objects too and add the properties to these copies. A neat way to do this is to use object spread in .map() callback:
.map(({ ...obj }) => {
obj.count = endorsements
.filter(x=>x.ideaNumber===obj.ideaNumber)
...
If your environment doesn't support object spread syntax, clone the object with Object.assign():
.map(originalObj => {
const obj = Object.assign({}, originalObj);
obj.count = endorsements
.filter(x=>x.ideaNumber===obj.ideaNumber)
...

In JS, the objects are referenced. When created, in other words, you get the object variable to point to a memory location which intends to be holding a meaningful value.
var o = {foo: 'bar'}
The variable o is now point to a memory which has {foo: bar}.
var p = o;
Now the variable p too is pointing to the same memory location. So, if you change o, it will change p too.
This is what happens inside your function. Even though you use Array methods which wouldn't mutate it's values, the array elements themselves are objects which are being modified inside the functions. It creates a new array - but the elements are pointing to the same old memory locations of the objects.
var a = [{foo: 1}]; //Let's create an array
//Now create another array out of it
var b = a.map(o => {
o.foo = 2;
return o;
})
console.log(a); //{foo: 2}
One way out is to create a new object for your new array during the operation. This can be done with Object.assign or latest spread operator.
a = [{foo: 1}];
b = a.map(o => {
var p = {...o}; //Create a new object
p.foo = 2;
return p;
})
console.log(a); // {foo:1}

To help having immutability in mind you could think of your values as primitives.
1 === 2 // false
'hello' === 'world' // false
you could extend this way of thinking to non-primitives as well
[1, 2, 3] === [1, 2, 3] // false
{ username: 'hitmands' } === { username: 'hitmands' } // false
to better understand it, please have a look at MDN - Equality Comparisons and Sameness
how to force immutability?
By always returning a new instance of the given object!
Let's say we have to set the property status of a todo. In the old way we would just do:
todo.status = 'new status';
but, we could force immutability by simply copying the given object and returning a new one.
const todo = { id: 'foo', status: 'pending' };
const newTodo = Object.assign({}, todo, { status: 'completed' });
todo === newTodo // false;
todo.status // 'pending'
newTodo.status // 'completed'
coming back to your example, instead of doing obj.count = ..., we would just do:
Object.assign({}, obj, { count: ... })
// or
({ ...obj, count: /* something */ })
there are libraries that help you with the immutable pattern:
Immer
ImmutableJS

You use the freeze method supplying the object you want to make immutable.
const person = { name: "Bob", age: 26 }
Object.freeze(person)

You could use the new ES6 built-in immutability mechanisms, or you could just wrap a nice getter around your objects similar to this
var myProvider = {}
function (context)
{
function initializeMyObject()
{
return 5;
}
var myImmutableObject = initializeMyObject();
context.getImmutableObject = function()
{
// make an in-depth copy of your object.
var x = myImmutableObject
return x;
}
}(myProvider);
var x = myProvider.getImmutableObject();
This will keep your object enclosed outside of global scope, but the getter will be accessible in your global scope.
You can read more on this coding pattern here

One easy way to make "copies" of mutable objects is to stringify them into another object and then parse them back into a new array. This works for me.
function returnCopy(arrayThing) {
let str = JSON.stringify(arrayThing);
let arr = JSON.parse(str);
return arr;
}

Actually, you can use spread opreator to make the original array stay unchanged, below y is the immutable array example:
const y = [1,2,3,4,5];
function arrayRotation(arr, r, v) {
for(let i = 0; i < r; i++) {
arr = [...y]; // make array y as immutable array [1,2,3,4,5]
arr.splice(i, 0, v);
arr.pop();
console.log(`Rotation ${i+1}`, arr);
}
}
arrayRotation(y, 3, 5)
If you don't use the spread operator, the y array will get changed when loop is running time by time.
Here is the mutable array result:
const y = [1,2,3,4,5];
function arrayRotation(arr, r, v) {
for(let i = 0; i < r; i++) {
arr = y; // this is mutable, because arr and y has same memory address
arr.splice(i, 0, v);
arr.pop();
console.log(`Rotation ${i+1}`, arr);
}
}
arrayRotation(y, 3, 5)

You assign these properties in your map function, you need to change this. (Just declare an empty object instead of using your current obj)

Related

How to duplicate elements in a js array without creating "dependent elements"?

I am trying to modify a single element from an array whose elements were previously duplicated n times. To perform the array duplication I just relied on a custom function duplicateElements(array, times)from this post (see #Bamieh answer). As shown in the exemple below, the problem is I can't modify a single element from the array without modifying other elements:
function duplicateElements(array, times) {
return array.reduce((res, current) => {
return res.concat(Array(times).fill(current));
}, []);
}
var myvar = duplicateElements([{ a: 1 }, { a: 2 }], 2);
myvar[0].a = 3;
console.log(myvar);
// (4) [{…}, {…}, {…}, {…}]
// 0: {a: 3}
// 1: {a: 3}
// 2: {a: 2}
// 3: {a: 2}
// length: 4
As you can see myvar[1].a was also modified although this wasn't intended. How can I avoid this issue?
The problem is that you're passing the reference to the original object in Array(times).fill(current) .
In this case the two copies of the first {a:2} are the same copy of the original (They reference to the same space in memory) so if you change one, the two of them will change as they reference the same object in memory.
You have to make a deepcloning function or maybe spread the object inside a new one. You can change your original function to work with objects and primitives like this:
function duplicateElements(elementsArray, times) {
//Make a new placeholder array
var newArray = [];
//Loop the array of elements you want to duplicate
for (let index = 0; index < elementsArray.length; index++) {
//Current element of the array of element
var currentElement = elementsArray[index];
//Current type of the element to check if it is an object or not
var currentType = typeof currentElement
//Loop over the times you want to copy the element
for (let index = 0; index < times; index++) {
//If the new element is not an object
if (currentType !== "object" && currentType){
//append the element
newArray.push(currentElement)
//if it is an Object
} else if (currentType === "object" && currentType){
//append an spreaded new Object https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Spread_syntax
newArray.push({...currentElement})
}
}
}
return newArray;
}
This is not the optimal way to do this, but I think that maybe you're new to javascript and is better to learn the old way of looping before using more Array functionalities (as the answer from Jonas Wilms, that is also a good answer).
I would recommend javascript.info and eloquent javascript to learn more about the language
The main reason for this as specified in the Array.fill documentation is that when dealing with objects it will copy by reference:
When fill gets passed an object, it will copy the reference and fill
the array with references to that object.
With lodash (and _.cloneDeep) that is one line like this:
let dubFn = (arr, t=1) =>
_.concat(arr, _.flatMap(_.times(t, 0), x => _.cloneDeep(arr)))
let r1 = dubFn([{a:1},{b:3}]) // no parameter would mean just 1 dub
let r2 = dubFn([{a:1},{b:3},5,[1]], 2) // 2 dublicates
r1[0].a = 3
r2[0].a = 3
console.log(r1)
console.log(r2)
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.11/lodash.min.js"></script>
Note that this now works with arrays/objects and primitives.
The idea is to use _.concat to return a new concatenated version of the input array with a combination of few functions which on the end return an array of cloned objects. We use _.times to return an array of in this case t elements and then for each of those elements we replace with a deep clone of the array. _.flatMap is needed to flatten the end result since we end up having array of arrays after the _.times call.
With ES6 you can do something like this:
let dubElements = (arr, t) =>
[...arr, ...new Array(t).fill().flatMap(x => arr.map(y => ({...y})))]
let r1 = dubElements([{a:1},{b:3}])
let r2 = dubElements([{a:1},{b:3}],2)
r1[0].a = 3
r2[0].a = 3
console.log(r1)
console.log(r2)
Where we concat arrays via the spread operator and we use new Array(t) to create the new duplicates array and make sure we fill it with undefined in this case after which we flatMap the results (which we map through the clone via the spread operator again.
Note that this works for your use case specifically. If you want to make it more generic you have to expand more in the last map function etc.
If you want to preserve the order of the elements you can do something like this:
let dubElements = (arr, t=1) => {
let _result = []
arr.forEach(x => {
for(let i=0; i<t+1; i++) {
_result.push({...x})
}
})
return _result
}
let result = dubElements([{a:1},{b:3}],2)
result[0].a = 3
console.log(result)
Replace
Array(times).fill(current)
which will add one reference to current multiple times to the array with:
Array.from({ length: times }, () => ({...current }))
which will shallow clone current. Note that the code will then only work with objects though, not with primitives.
I'd do:
const duplicateElements = (array, length) =>
array.flatMap(current => Array.from({ length }, () => ({ ...current }));

Modify an array without mutation

I am trying to solve a problem which states to remove(delete) the smallest number in an array without the order of the elements to the left of the smallest element getting changed . My code is -:
function removeSmallest(numbers){
var x = Math.min.apply(null,numbers);
var y = numbers.indexOf(x);
numbers.splice(y,1);
return numbers;
}
It is strictly given in the instructions not to mutate the original array/list. But I am getting an error stating that you have mutated original array/list .
How do I remove the error?
Listen Do not use SPLICE here. There is great known mistake rookies and expert do when they use splice and slice interchangeably without keeping the effects in mind.
SPLICE will mutate original array while SLICE will shallow copy the original array and return the portion of array upon given conditions.
Here Slice will create a new array
const slicedArray = numbers.slice()
const result = slicedArray.splice(y,1);
and You get the result without mutating original array.
first create a copy of the array using slice, then splice that
function removeSmallest(numbers){
var x = Math.min.apply(null,numbers);
var y = numbers.indexOf(x);
return numbers.slice().splice(y,1);
}
You can create a shallow copy of the array to avoid mutation.
function removeSmallest(numbers){
const newNumbers = [...numbers];
var x = Math.min.apply(null,newNumbers);
var y = newNumbers.indexOf(x);
newNumbers.splice(y,1);
return newNumbers;
}
array.slice() and [... array] will make a shallow copy of your array object.
"shallow" the word says itself.
in my opinion, for copying your array object the solution is:
var array_copy = copy(array);
// copy function
function copy(object) {
var output, value, key;
output = Array.isArray(object) ? [] : {};
for (key in object) {
value = object[key];
output[key] = (typeof value === "object") ? copy(value) : value;
}
return output;
}
Update
Alternative solution is:-
var arr_copy = JSON.parse(JSON.stringify(arr));
I'm not sure what the exact context of the problem is, but the goal might be to learn to write pure transformations of data, rather than to learn how to copy arrays. If this is the case, using splice after making a throwaway copy of the array might not cut it.
An approach that mutates neither the original array nor a copy of it might look like this: determine the index of the minimum element of an array, then return the concatenation of the two sublists to the right and left of that point:
const minIndex = arr =>
arr.reduce(
(p, c, i) => (p === undefined ? i : c < arr[p] ? i : p),
undefined
);
const removeMin = arr => {
const i = minIndex(arr);
return minIndex === undefined
? arr
: [...arr.slice(0, i), ...arr.slice(i + 1)];
};
console.log(removeMin([1, 5, 6, 0, 11]));
Let's focus on how to avoid mutating. (I hope when you say "remove an error" you don't mean "suppress the error message" or something like that)
There are many different methods on Array.prototype and most don't mutate the array but return a new Array as a result. say .map, .slice, .filter, .reduce
Telling the truth just a few mutate (like .splice)
So depending on what your additional requirements are you may find, say .filter useful
let newArray = oldArray.filter(el => el !== minimalElementValue);
or .map
let newArray = oldArray.map(el => el === minimalElementValue? undefined: el);
For sure, they are not equal but both don't mutate the original variable

How to edit value of an object using the reference?

I have a big object array persons
persons = [{name:'john1'}, {name:'john2'},...]
I iterated the array and found the object I am interested to edit
objectToEdit = persons .find((person)=>person.name==='john1')
Now I created an edited object in immutable way (someOtherPerson = {name:'johnxx'})
objectFinal = {...objectToEdit, someOtherPerson}
Now I want to replace this objectFinal with objectToEdit in persons array, without having to traverse the array again. But doing objectToEdit =objectFinal , will just assign objectToEdited's reference to objectToEdit , without making any change in the persons array
Is there a clean way to achieve this without traversing the array?
Edit:
In this example, the object in persons jave just one key (i.e, name). This is to make question minimal. In my project, I have more than 30 keys.
If you want to edit an object in a list,in place, use Array.prototype.some
var persons = [{
name: 'john1'
}, {
name: 'jack5'
}]
var someOtherPerson = {
name: 'johnxx'
}
persons.some(function(person) {
// if condition, edit and return true
if (person.name === 'john1') {
// use Object.keys to copy properties
Object.keys(someOtherPerson).forEach(function(key) {
person[key] = someOtherPerson[key]
})
// or use assign to merge 2 objects
Object.assign(person, someOtherPerson);
return true // stops iteration
}
})
console.log(JSON.stringify(persons))
If you want to avoid mutating the objects in the original array, you might use .findIndex instead, and reassign the item in the array at that index:
const persons = [{name:'john1'}, {name:'john2'}];
const objectToEditIndex = persons.findIndex((person) => person.name === 'john1');
const someOtherPerson = {name:"johnxx"};
persons[objectToEditIndex] = {
...persons[objectToEditIndex],
...someOtherPerson
};
console.log(persons);
Doing this:
objectFinal = {...objectToEdit, someOtherPerson}
you lose the reference to the original objectToEdit object. To edit it, you can do
`objectToEdit.value = otherValue`.
PS: Having said that, you only lose reference to the first level object. If that object, contains another object or array, you have reference to it:
objectFinal.innerObject.value = otherValue;
{name:'john1'} should be replace with {name:"johnxx"} in persons
Reassign persons to the persons-array mapped with the new value:
let persons = [{name:'john1'}, {name:'john2'}];
persons = persons.map( v => v.name === "john1" ? {name: "johnxx"} : v );
console.log(persons);
Or, if you're worried about performance, use Array.splice in combination with Array.findIndex
The findIndex method executes the callback function once for every
array index 0..length-1 (inclusive) in the array until it finds one
where callback returns a truthy value (a value that coerces to true).
If such an element is found, findIndex immediately returns the index
for that iteration.
let persons = [{name:'john1'}, {name:'john2'}];
persons.splice(persons.findIndex( v => v.name==="john1" ), 1, {name:'johnxx'});
console.log(persons);
Or use a utility method (snippet for a large array (1,000,000 elements) and with performance timer)
// replacer method
const replace = (obj, [key, value], replacement) => {
obj.splice(persons.findIndex( v => v[key] === value ), 1, replacement);
return obj;
}
// create a large array (this may take some extra time)
let persons = Array.from({length: 1000000}).map(v =>
({name: Math.floor(100000 + Math.random() * 1000000000).toString(16)}) );
// pick an entry
const someEntry = Math.floor(persons.length * Math.random());
const entry = Object.entries(persons[someEntry])[0];
console.log("before:", persons[someEntry]);
const t = performance.now();
// replacement call here
console.log("after:", replace(persons, entry, {name: "johnxx"})[someEntry]);
console.log("replacement took:", `${((performance.now() - t)/1000).toFixed(3)} sec`);

Why does changing the value of a property change the value for all similarly named properties in an array?

Why does changing the value of a property change the value for all similar properties in an array and how do I get it to work right without using the this keyword for 'name'?
let Object = {
'name' : 'Test Object'
}
let Array = []
Array.push(Object)
Array.push(Object)
Array.push(Object)
Array[0]['name'] = 'Changed'
console.log(Array) // expect only the first name to change, but all 3 change...
You aren't changing "similarly named" objects, you are changing the same object.
For non-primitives (basically everything that isn't a string, number, or boolean), they are passed by reference. That means when you add them to something like an array or pass them to a function, you are basically passing their address. If you pass it 3 times, they all point to the same address; there is still only one copy. Change one, and you change them all.
const a = { b: 1 };
const arr = [a, a, a];
// All the same object
console.log(arr[0] === arr[1], arr[1] === arr[2], a === arr[0]);
a.b = 5;
// All 3 changed, because it is the same thing
console.log(arr.map(a => a.b));
function someFunc(obj) { obj.b = 10 };
someFunc(a);
// changed from inside function, same object
console.log(a.b);
If you want to create a handful of objects that all start the same, but then are able to change afterwards, you need to create the objects in a loop:
const template = { name: 'a' };
const arr = [];
for (let i = 0; i < 3; i++) {
arr.push({ ...template }); // or: arr.push(Object.create({}, template))
}
arr[1].name = 'b';
arr[2].name = 'c';
console.log(arr);
Or, even more concisely:
// Creates a new Array with 3 records and then puts a copy of the template in each.
const template = { name: 'a' };
const arr = new Array(3).fill(1).map(() => ({ ...template }));
// or (without needing template variable):
// const arr = new Array(3).fill(1).map(() => ({ name: 'a' }))
arr[1].name = 'b';
arr[2].name = 'c';
console.log(arr);
When you invoke Array.push(Object), you're pushing a reference to the same object into the array 3 times. Push does not make a copy of Object - only 1 Object exists.
If you want 3 identical objects in an array, try something like this:
let vArray = []
for(i = 0; i <= 2; i++) {
//We're going to loop this 3 times, creating 3 different
//objects and pushing each of them into the array
let vObject = {
'name' : 'Test Object'
}
vArray.push(vObject)
}
vArray[0]['name'] = 'Changed'
console.log(vArray) // Only the first one will have been changed.
The answer I was looking for was to change the syntax in my answer to Array.push({...Object}). This creates a 'new' object to be pushed, with only 5 additional characters...
I didn't know 'object spread syntax' essentially did this.

Why does a js map on an array modify the original array?

I'm quite confused by the behavior of map().
I have an array of objects like this:
const products = [{
...,
'productType' = 'premium',
...
}, ...]
And I'm passing this array to a function that should return the same array but with all product made free:
[{
...,
'productType' = 'free',
...
}, ...]
The function is:
const freeProduct = function(products){
return products.map(x => x.productType = "free")
}
Which returns the following array:
["free", "free", ...]
So I rewrote my function to be:
const freeProduct = function(products){
return products.map(x => {x.productType = "free"; return x})
}
Which returns the array as intended.
BUT ! And that's the moment where I loose my mind, in both cases my original products array is modified.
Documentation around map() says that it shouldn't ( https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/map ).
I even tried to create a clone of my array turning my function into this:
const freeProduct = function(products){
p = products.splice()
return p.map(x => {x.productType = "free"; return x})
}
But I still get the same result (which starts to drive me crazy).
I would be very thankful to anyone who can explain me what I'm doing wrong!
Thank you.
You're not modifying your original array. You're modifying the objects in the array. If you want to avoid mutating the objects in your array, you can use Object.assign to create a new object with the original's properties plus any changes you need:
const freeProduct = function(products) {
return products.map(x => {
return Object.assign({}, x, {
productType: "free"
});
});
};
2018 Edit:
In most browsers you can now use the object spread syntax instead of Object.assign to accomplish this:
const freeProduct = function(products) {
return products.map(x => {
return {
...x,
productType: "free"
};
});
};
To elaborate on SimpleJ's answer - if you were to === the two arrays, you would find that they would not be equal (not same address in memory) confirming that the mapped array is in fact a new array. The issue is that you're returning a new array, that is full of references to the SAME objects in the original array (it's not returning new object literals, it's returning references to the same object). So you need to be creating new objects that are copies of the old objects - ie, w/ the Object.assign example given by SimpleJ.
Unfortunately, whether the spread operator nor the object assign operator does a deep copy.... You need to use a lodash like function to get areal copy not just a reference copy.
const util = require('util');
const print = (...val) => {
console.log(util.inspect(val, false, null, false /* enable colors */));
};
const _ = require('lodash');
const obj1 = {foo:{bar:[{foo:3}]}};
const obj2 = {foo:{bar:[{foo:3}]}};
const array = [obj1, obj2];
const objAssignCopy = x => { return Object.assign({}, x, {})};
const spreadCopy = x => { return {...x}};
const _Copy = x => _.cloneDeep(x);
const map1 = array.map(objAssignCopy);
const map2 = array.map(spreadCopy);
const map3 = array.map(_Copy);
print('map1', map1);
print('map2', map2);
print('map3', map3);
obj2.foo.bar[0].foo = "foobar";
print('map1 after manipulation of obj2', map1); // value changed
print('map2 after manipulation of obj2', map2); // value changed
print('map3 after manipulation of obj2', map3); // value hasn't changed!
Array Iterator Array.map() creates the new array with the same number of elements or does not change the original array. There might be the problem with referencing if there is object inside the array as it copies the same reference, so, when you are making any changes on the property of the object it will change the original value of the element which holds the same reference.
The solution would be to copy the object, well, array.Splice() and [...array](spread Operator) would not help in this case, you can use JavaScript Utility library like Loadash or just use below mention code:
const newList = JSON.parse(JSON.stringify(orinalArr))
Array Destructuring assignment can be used to clone the object.
const freeProduct = function(products){
p = products.splice()
return p.map(({...x}) => {x.productType = "free"; return x})
}
This method will not modify the original object.

Categories

Resources