JS forEach traverses the problem of modifying objects - javascript

Why does the first modification work (when I reassign properties of the object) but not the second (when I reassign the whole object)?
const arr1 = [
{ id: 1, value: 1 },
{ id: 2, value: 2 },
{ id: 3, value: 3 },
{ id: 4, value: 4 },
{ id: 5, value: 5 },
]
arr1.forEach((item, index) => {
if (item.id === 1) {
item.value *= 10 // modify success
}
});
console.log(arr1);
arr1.forEach((item, index) => {
if (item.id === 1) {
item = {id:6,value:6} // modify fail
}
});
console.log(arr1);
.as-console-wrapper { max-height: 100% !important; top: auto; }

Consider the example below:
We create an object and assign it to variable foo and then we assign foo to bar. So, now both foo and bar refer to the same object, as illustrated in the diagram below.
let foo = { id: 1, val: "foo" };
let bar = foo;
Next let's change the val field of the object i.e. assigned to bar. We notice that the change is reflected by both the variables foo and bar and this is because both the variables refer to the same object.
let foo = {id: 1,val: "foo"};
let bar = foo;
bar.val = "bar";
console.log(foo, bar);
Next we assign a new object to bar. Notice this doesn't effect the object that foo refers to, bar is simply now referring to a different object.
let foo = { id: 1, val: "foo" };
let bar = foo;
bar = { id: 1, val: "bar" };
console.log(foo, bar);
Let's relate this to the forEach example in your question. So, in every iteration of the forEach loop, the item argument in the callback function points to an object from the array and when you change a field from this item argument it changes the object in the array but when you assign item to a new object it does nothing to the object stored in the array.
If you want to replace the entire object with a new one, there are several approaches you could take, two of which are mentioned below:
Finding the index where the object is stored and replacing it with a new object.
const arr = [{ id: 1, value: 1 }, { id: 2, value: 2 }, { id: 3, value: 3 }, { id: 4, value: 4 }, { id: 5, value: 5 }];
const index = arr.findIndex((obj) => obj.id === 3);
if (index !== -1) {
arr[index] = { id: 6, value: 6 };
}
console.log(arr);
Another really common approach is to map over the array and create a new array with that one object replaced.
const arr = [{ id: 1, value: 1 }, { id: 2, value: 2 }, { id: 3, value: 3 }, { id: 4, value: 4 }, { id: 5, value: 5 }];
const newArr = arr.map((obj) => (obj.id === 3 ? { id: 6, value: 6 } : obj));
console.log(newArr);

Your confusion is natural, and it's to do with the difference between the actual object, and the variable.
In your foreach function, you have a variable "item" which points to the object in the array you are looping. But when you run
item = {id:6,value:6}
You are not modifying the object, but rather making the variable "item" point to a new object that you've just created.
If you want to change the actual object itself, you can do it manually by modifying the values one by one, or by copying from another object using Object.assign.
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/assign
In this snippet, I've made a few examples that hopefully illustrate the differences, and at the end your specific case being solved using Object.assign()
const a = { id: 1, value: 1 };
let b = a; // b now points to the same object as variable a
b = {id: 2, value: 2};
console.log('a', a, 'b', b) // output shows two different objects with different values
const c = {id: 3, value: 3};
const d = c;
Object.assign(c, {id: 4, value: 4});
console.log('c', c, 'd', d); // c and d both have id: 4 and value: 4 because they both point to the same object
const e = { id: 5, value: 5 };
const f = e;
e.id = 6;
console.log('e', e, 'f', f); // directly assigning the value also works
const arr1 = [
{ id: 1, value: 1 },
{ id: 2, value: 2 },
{ id: 3, value: 3 },
{ id: 4, value: 4 },
{ id: 5, value: 5 },
]
arr1.forEach((item, index) => {
if (item.id === 1) {
item.value *= 10 // modify success
}
})
arr1.forEach((item, index) => {
if (item.id === 1) {
Object.assign(item, {id:6,value:6}) // success with assign
}
})
console.log(arr1)

Because item is a reference to the object that's currently being iterated over, modifying the reference will in turn modify the original object (hence the behaviour observed in your first code example). In the second forEach you just reassign the reference to a brand new object so you can no longer modify the original object you were iterating over.
Basically, when you loop over the array with forEach, item is a variable whose current value is a reference/pointer to the object currently being iterated over. Modifying this object modifies the original (as it's just a reference, not an actual cloned object). However, when you reassign it, the reference is no longer present because item has a new value that it points to. You could modify it like so, with success:
const arr1 = [
{ id: 1, value: 1 },
{ id: 2, value: 2 },
{ id: 3, value: 3 },
{ id: 4, value: 4 },
{ id: 5, value: 5 },
];
arr1.forEach((item, index) => {
if (item.id === 1) {
item.value *= 10 // modify success
}
});
console.log(arr1);
arr1.forEach((item, index) => {
if (item.id === 1) {
item.id = 6;
item.value = 6;
}
});
console.log(arr1);
.as-console-wrapper { max-height: 100% !important; top: auto; }
You could do it dynamically as well, by looping over each property you wish to reassign like so:
const arr1 = [
{ id: 1, value: 1 },
{ id: 2, value: 2 },
{ id: 3, value: 3 },
{ id: 4, value: 4 },
{ id: 5, value: 5 },
];
arr1.forEach((item, index) => {
if (item.id === 1) {
item.value *= 10 // modify success
}
});
console.log(arr1);
arr1.forEach((item, index) => {
if (item.id === 1) {
let toConvert = { id: 6, value: 6};
Object.entries(toConvert).forEach(([k, v]) => item[k] = v);
}
});
console.log(arr1);
.as-console-wrapper { max-height: 100% !important; top: auto; }

For the first case, it can be understood as:
const it = { value: 10 };
const item = it;
item.value = 11;
console.log(it); // item and it point to the same object, so the object's property value is modified
second case:
const it = { value: 10 };
let item = it; // Here item and it refer to the same object
item = { value: 6 }; // But here item points to the new object (direct assignment), and it still points to the original object
console.log(item, it); // So it is still { value: 10 }, and item is already { value: 6 }

Related

How to add all numerical key-values in array of objects?

If you have an array of objects like so:
What's the best way to add all numerical values in each object so each one looks something like this:
{category: "A", total: 44}
So in the 0th item in the original array, 0+23+21 is 24, and is now represented by the new 'total' key.
Bearing in mind that the 'keys' with numerical values in the original array e.g. 'col2' are randomly generated (so another array like the original can have keys like 'somethingelse'.
I've attempted it with the following, but I believe it's not written correctly:
newArrayOfObjects.forEach(element => {
Object.values(element).reduce((a, b) => a + b);
});
It may be good to know but the 'key' category always exists in each object and is fixed. All other key values are numerical and there'll always be more than one.
Please check this.
const array = [
{
category: 'A',
col1: 1,
col2: 2,
col3: 3,
},
{
category: 'B',
col1: 2,
col2: 3,
col3: 4,
}
]
const result = array.map(obj => {
const total = Object.values(obj).reduce((acc, value) => {
if (typeof value === 'number') {
return acc + value;
}
return acc;
}, 0)
return {
category: obj.category,
total
}
})
console.log(result)
You could use Array.map() along with Array.reduce() to sum the numeric values in the array.
We'd create a toNumber() function to get the numeric value of any property. If this is not a number, it will return 0 (keeping the total unchanged).
let arr = [
{ a: 0, category: "a", col2: 23, col3: 21 },
{ b: 0, category: "b", x: 100, y: 10, z: 1 },
{ j: 0, category: "x", foo: 25, bar: 50, meta: 'content' },
]
function toNumber(n) {
return isNaN(n) ? 0: n;
}
function sumTotals(a) {
return a.map(({ category, ...obj}) => {
const total = Object.values(obj).reduce((total, value) => {
return total + toNumber(value);
}, 0);
return { category, total };
})
}
console.log('Totals:', sumTotals(arr))
.as-console-wrapper { max-height: 100% !important; }
arr = [{x:1}, {x:3}]
arr.reduce((accumulator, current) => accumulator + current.x, 0);
var data = [
{ "category": "A", "col0": 5, "col1": 8, "some": "thing"},
{ "category": "B", "col1": 3, "col2": 5}
];
var res = data.map((it) => {
const { category, ...rest } = it;
return {
...it,
total: Object.values(rest).reduce(
(prev, curr) =>
typeof curr === "number" ? prev + curr : prev, // add if the current value is numeric
0
)
}
});
console.log(res);
/**
[
{"category":"A","col0":5,"col1":8,"some":"tst","total":13},
{"category":"B","col1":3,"col2":5,"total":8}
]
**/
I think you are on the right way, you just need to do a bit more destructuring and type checking:
const aggregated = newArrayOfObjects.map((obj) =>
Object.entries(obj).reduce(
(newObj, [key, value]) => ({
...newObj,
...(typeof value === "number"
? { total: newObj.total + value }
: { [key]: value }),
}),
{ total: 0 }
)
);
First, you map all objects to their representations as key-value-pairs. Then you iterate over these key-value pairs and keep all non-numerical values and their respective keys, while dropping key-value-pairs with a numerical value and replacing them by a property in which you aggregate the total value.

Value change in 1 object changes in all objects in array

I have an array of objects. Each object has a key quantity and value. I want to duplicate each object in the array based on its quantity. Next, I want to manipulate only one of the duplicated object in the array. But on manipulating value of 1 object, value of all duplicated objects change. Here is my code:
let arr = [
{ id: 1, quantity: 3, value: 10 },
{ id: 2, quantity: 1, value: 5 },
{ id: 2, quantity: 5, value: 5 },
];
const newArr = [];
for (const a of arr) {
if (a.quantity > 1) {
let quantity = a.quantity;
a.quantity = 1;
while (quantity--) {
newArr.push(a);
}
}
}
arr = newArr;
arr[0].value = 1;
When I changed the value of arr[0] to 1, value field of arr[1] and arr[2] also changed to 1.
I have tried copying the object using spread operator and JSON.parse(JSON.parse()), but none has worked.
Because newArr.push(a) .a push to newArr ref to element of arr
You can edit same as :
let arr = [
{ id: 1, quantity: 3, value: 10 },
{ id: 2, quantity: 1, value: 5 },
{ id: 2, quantity: 5, value: 5 },
]
const newArr = []
for (const a of arr) {
if (a.quantity > 1) {
let quantity = a.quantity;
a.quantity = 1;
while (quantity--) {
newArr.push({...a})
}
}
}
arr = [...newArr]
arr[0].value = 1
console.log(arr)
// example for Memory Management
let a = { id: 1, quantity: 3, value: 10 }
let b = { id: 1, quantity: 3, value: 10 }
let c = arr[0]
let d = {...arr[0]}
console.log(a === arr[0]) // false : different allocates memory for contain value
console.log(a === b) // false : different allocates memory for contain value
console.log(c === arr[0]) // true : refer to a memory
console.log(d === arr[0]) // false : different allocates memory for contain value

Remove all keys that have the same value in array

I need to remove all keys except last inserted that have the same value on key id_ask in array but I'm learning javascript and I still do not know how to do this.
jQuery(function()
{
let arr = []
let q = []
$("body").on('click', '.link_resposta', function(event)
{
event.preventDefault();
/* Act on the event */
let id_poll = $(this).data("idpesquisa")
let id_ask = $(this).data("idpergunta")
let id_anwser = $(this).children("li").data("idresposta")
let q = {
id_poll,
id_ask,
id_anwser
}
arr.push(q)
console.log(arr)
});
});
Using a combination of Set, Array.reverse() and Array.map we can solve this easily.
We first use the Set and we map our source array in, just feeding the id_ask field. From that we get an array of unique id_ask.
We then map the unique id_ask array and for each id_ask we call a find() on the source array in reverse.
Comments inline.
const sampleArray = [
{
id: 1,
id_ask: 2,
id_answer: 3
},
{
id: 2,
id_ask: 2,
id_answer: 5
},
{
id: 3,
id_ask: 3,
id_answer: 3
},
{
id: 4,
id_ask: 3,
id_answer: 1
},
{
id: 5,
id_ask: 4,
id_answer: 3
}
];
// Create a unique Set of Ask ID
const uniqueAskId = [...new Set(sampleArray.map(e => e.id_ask))];
console.log(uniqueAskId);
// Use Map and Reverse to get last item.
const r = uniqueAskId.map(uid => sampleArray.reverse().find(ask => ask.id_ask === uid));
console.log(r);
Here it is as a single statement:
const sampleArray = [
{
id: 1,
id_ask: 2,
id_answer: 3
},
{
id: 2,
id_ask: 2,
id_answer: 5
},
{
id: 3,
id_ask: 3,
id_answer: 3
},
{
id: 4,
id_ask: 3,
id_answer: 1
},
{
id: 5,
id_ask: 4,
id_answer: 3
}
];
// put together in a single statement.
const result = [...new Set(sampleArray.map(e => e.id_ask))]
.map(uid => sampleArray.reverse().find(ask => ask.id_ask === uid));
console.log(result);
NOTE: For large datasets it would obviously be more efficient to call the reverse() one time before you use.
const revArray = myArray.reverse();
const resultArray = [...new Set(revArray.map(e => e.id_ask))]
.map(uid => revArray.reverse().find(ask => ask.id_ask === uid));

Combine object and arrays

I'm trying to write a function that takes an array of objects, and an unlimited number of arrays, and combines them to form a single object. The inputs would follow this pattern:
let x = [{ name: 'Tom' }, { name: 'John' }, { name: 'Harry' }];
let y = [[1, 2, 3], 'id'];
let z = [['a', 'b', 'c'], 'value'];
combine(x, y, z);
With the second element of y and z acting as the object key. Using these arguments, the function should return the following array:
[
{
name: 'Tom',
id: 1,
value: 'a'
},
{
name: 'John',
id: 2,
value: 'b'
},
{
name: 'Harry',
id: 3,
value: 'c'
},
]
The index of the current object should be used to get the correct element in the array. I have made an attempt at the problem:
function combine(object, ...arrays) {
return object.map((obj, index) => {
let items = arrays.map(arr => ({
[arr[1]]: arr[0][index]
}));
return Object.assign({}, obj, { items });
});
}
This almost does the job, but results in the array items being hidden inside a nested items array, How can I solve this?
You had been assigning an object of object, and the result was a new object with the element items inside (another feature of object literal).
This approach use reduce instead of map and direct assign instead of object literal.
function combine(object, ...arrays) {
return object.map((obj, index) => {
const items = arrays.reduce((acc, arr) => {
acc[arr[1]] = arr[0][index] ;
return acc;
}, {});
return Object.assign({}, obj, items);
});
}
const x = [{ name: 'Tom' }, { name: 'John' }, { name: 'Harry' }];
const y = [[1, 2, 3], 'id'];
const z = [['a', 'b', 'c'], 'value'];
combine(x, y, z);
You can also use the spread operator in the Object.assign, like this:
function combine(object, ...arrays) {
return object.map((obj, index) => {
let items = arrays.map(arr => ({
[arr[1]]: arr[0][index]
}));
return Object.assign({}, obj, ...items);
});
}
This almost does the job, but results in the array items being hidden inside a nested items array
The problem is that items is an array, whereas you only need the current item inside of that particular map callback. No need to nest loops here.
Also I would recommend avoiding multiple properties per combine call. The resulting code would look like this:
function combine(objects, [values, key]) {
return objects.map((o, i) =>
Object.assign({[key]: values[i]}, o)
);
}
combine(combine(x, y), z);
If you then have multiple extensions to do, you can also use
[y, z].reduce(combine, x)
With map and computed keys, you can achieve this.
Here's a working example:
let x = [{
name: 'Tom'
}, {
name: 'John'
}, {
name: 'Harry'
}];
let y = [[1, 2, 3], 'id'];
let z = [['a', 'b', 'c'], 'value'];
let result = [];
x.map(function (el, index) {
result.push(el);
let index = result.length -1;
result[index][y[1]] = y[0][index];
result[index][z[1]] = z[0][index];
});
console.log(result);

Object array clone with subset of properties

What is the most efficient way in JavaScript to clone an array of uniform objects into one with a subset of properties for each object?
UPDATE
Would this be the most efficient way to do it or is there a better way? -
var source = [
{
id: 1,
name: 'one',
value: 12.34
},
{
id: 2,
name: 'two',
value: 17.05
}
];
// copy just 'id' and 'name', ignore 'value':
var dest = source.map(function (obj) {
return {
id: obj.id,
name: obj.name
};
});
using Object Destructuring and Property Shorthand
let array = [{ a: 5, b: 6, c: 7 }, { a: 8, b: 9, c: 10 }];
let cloned = array.map(({ a, c }) => ({ a, c }));
console.log(cloned); // [{ a: 5, c: 7 }, { a: 8, c: 10 }]
First define a function that clone an object and return a subset of properties,
Object.prototype.pick = function (props) {
return props.reduce((function (obj, property) {
obj[property] = this[property];
return obj;
}).bind(this), {});
}
Then define a function that clone an array and return the subsets of each object
function cloneArray (array, props) {
return array.map(function (obj) {
return obj.pick(props);
});
}
Now let's say you have this array :
var array = [
{ name : 'khalid', city : 'ifrane', age : 99 },
{ name : 'Ahmed', city : 'Meknes', age : 30 }
];
you need to call the function and pass the array of properties you need to get as result
cloneArray(array, ['name', 'city']);
The result will be :
[
{ name : 'khalid', city : 'ifrane' },
{ name : 'Ahmed', city : 'Meknes' }
]
Performance-wise, that would be the most efficient way to do it, yes.

Categories

Resources