I'm currently learning about the reduce method in JS, and while I have a basic understanding of it, more complex code completely throws me off. I can't seem to wrap my head around how the code is doing what it's doing. Mind you, it's not that the code is wrong, it's that I can't understand it. Here's an example:
const people = [
{ name: "Alice", age: 21 },
{ name: "Max", age: 20 },
{ name: "Jane", age: 20 },
];
function groupBy(objectArray, property) {
return objectArray.reduce((acc, obj) => {
const key = obj[property];
const curGroup = acc[key] ?? [];
return { ...acc, [key]: [...curGroup, obj] };
}, {});
}
const groupedPeople = groupBy(people, "age");
console.log(groupedPeople);
// {
// 20: [
// { name: 'Max', age: 20 },
// { name: 'Jane', age: 20 }
// ],
// 21: [{ name: 'Alice', age: 21 }]
// }
Now the reduce method as I understand it, takes an array, runs some provided function on all the elements of the array in a sequential manner, and adds the result of every iteration to the accumulator. Easy enough. But the code above seems to do something to the accumulator as well and I can't seem to understand it. What does
acc[key] ?? []
do?
Code like this make it seem like a breeze:
const array1 = [1, 2, 3, 4];
// 0 + 1 + 2 + 3 + 4
const initialValue = 0;
const sumWithInitial = array1.reduce(
(accumulator, currentValue) => accumulator + currentValue,
initialValue
);
console.log(sumWithInitial);
// Expected output: 10
But then I see code like in the first block, I'm completely thrown off. Am I just too dumb or is there something I'm missing???
Can someone please take me through each iteration of the code above while explaining how it
does what it does on each turn? Thanks a lot in advance.
You are touching on a big problem with reduce. While it is such a nice function, it often favors code that is hard to read, which is why I often end up using other constructs.
Your function groups a number of objects by a property:
const data = [
{category: 'catA', id: 1},
{category: 'catA', id: 2},
{category: 'catB', id: 3}
]
console.log(groupBy(data, 'category'))
will give you
{
catA: [{category: 'catA', id: 1}, {category: 'catA', id: 2}],
catB: [{category: 'catB', id: 3}]
}
It does that by taking apart the acc object and rebuilding it with the new data in every step:
objectArray.reduce((acc, obj) => {
const key = obj[property]; // get the data value (i.e. 'catA')
const curGroup = acc[key] ?? []; // get collector from acc or new array
// rebuild acc by copying all values, but replace the property stored
// in key with an updated array
return { ...acc, [key]: [...curGroup, obj] };
}, {});
You might want to look at spread operator (...) and coalesce operator (??)
Here is a more readable version:
objectArray.reduce((groups, entry) => {
const groupId = entry[property];
if(!groups[groupId]){
groups[groupId] = [];
}
groups[groupId].push(entry);
return groups;
}, {});
This is a good example where I would favor a good old for:
function groupBy(data, keyProperty){
const groups = {}
for(const entry of data){
const groupId = entry[keyProperty];
if(!groups[groupId]){
groups[groupId] = [];
}
groups[groupId].push(entry);
}
return groups;
}
Pretty much the same number of lines, same level of indentation, easier to read, even slightly faster (or a whole lot, depending on data size, which impacts spread, but not push).
That code is building an object in the accumulator, starting with {} (an empty object). Every property in the object will be a group of elements from the array: The property name is the key of the group, and the property value is an array of the elements in the group.
The code const curGroup = acc[key] ?? []; gets the current array for the group acc[key] or, if there isn't one, gets a new blank array. ?? is the "nullish coalescing operator." It evaluates to its first operand if that value isn't null or undefined, or its second operand if the first was null or undefined.
So far, we know that obj[property] determines the key for the object being visited, curGroup is the current array of values for that key (created as necessary).
Then return { ...acc, [key]: [...curGroup, obj] }; uses spread notation to create a new accumulator object that has all of the properties of the current acc (...acc), and then adds or replaces the property with the name in key with a new array containing any previous values that the accumulator had for that key (curGroup) plus the object being visited (obj), since that object is in the group, since we got key from obj[property].
Here's that again, related to the code via comments. I've split out the part creating a new array [...curGroup, obj] from the part creating a new accumulator object for clarity:
function groupBy(objectArray, property) {
return objectArray.reduce(
(acc, obj) => {
// Get the value for the grouping property from this object
const key = obj[property];
// Get the known values array for that group, if any, or
// a blank array if there's no property with the name in
// `key`.
const curGroup = acc[key] ?? [];
// Create a new array of known values, adding this object
const newGroup = [...curGroup, obj];
// Create and return a new object with the new array, either
// adding a new group for `key` or replacing the one that
// already exists
return { ...acc, [key]: newGroup };
},
/* The starting point, a blank object: */ {}
);
}
It's worth noting that this code is very much written with functional programming in mind. It uses reduce instead of a loop (when not using reduce, FP usually uses recursion rather than loops) and creates new objects and arrays rather than modifying existing ones.
Outside of functional programming, that code would probably be written very differently, but reduce is designed for functional programming, and this is an example of that.
Just FWIW, here's a version not using FP or immutability (more on immutability below):
function groupBy(objectArray, property) {
// Create the object we'll return
const result = {};
// Loop through the objects in the array
for (const obj of objectArray) {
// Get the value for `property` from `obj` as our group key
const key = obj[property];
// Get our existing group array, if we have one
let group = result[key];
if (group) {
// We had one, add this object to it
group.push(obj);
} else {
// We didn't have one, create an array with this object
// in it and store it on our result object
result[key] = [obj];
}
}
return result;
}
In a comment you said:
I understand the spread operator but it's use in this manner with the acc and the [key] is something I'm still confused about.
Yeah, there are a lot of things packed into return { ...acc, [key]: [...curGroup, obj] };. :-) It has both kinds of spread syntax (... isn't an operator, though it's not particularly important) plus computed property name notation ([key]: ____). Let's separate it into two statements to make it easier to discuss:
const updatedGroup = [...curGroup, obj];
return { ...acc, [key]: updatedGroup };
TL;DR - It creates and returns a new accumulator object with the contents of the previous accumulator object plus a new or updated property for the current/updated group.
Here's how that breaks down:
[...curGroup, obj] uses iterable spread. Iterable spread spreads out the contents of an iterable (such as an array) into an array literal or a function call's argument list. In this case, it's spread into an array literal: [...curGroup, obj] says "create a new array ([]) spreading out the contents of the curGroup iterable at the beginning of it (...curGroup) and adding a new element at the end (, obj).
{ ...acc, ____ } uses object property spread. Object property spread spreads out the properties of an object into a new object literal. The expression { ...acc, _____ } says "create a new object ({}) spreading out the properties of acc into it (...acc) and adding or updating a property afterward (the part I've left as just _____ for now)
[key]: updatedGroup (in the object literal) uses computed property name syntax to use the value of a variable as the property name in an object literal's property list. So instead of { example: value }, which creates a property with the actual name example, computed property name syntax puts [] around a variable or other expression and uses the result as the property name. For instance, const obj1 = { example: value }; and const key = "example"; const obj2 = { [key]: value }; both create an object with a propety called example with the value from value. The reduce code is using [key]: updatedGroup] to add or update a property in the new accumulator whose name comes from key and whose value is the new group array.
Why create a new accumulator object (and new group arrays) rather than just updating the one that the code started with? Because the code is written such that it avoids modifying any object (array or accumulator) after creating it. Instead of modifying one, it always creates a new one. Why? It's "immutable programming," writing code that only ever creates new things rather than modifying existing things. There are good reasons for immutable programming in some contexts. It reduces the possibilities of a change in code in one place from having unexpected ramifications elsewhere in the codebase. Sometimes it's necessary, because the original object is immutable (such as one from Mongoose) or must be treated as though it were immutable (such as state objects in React or Vue). In this particular code it's pointless, it's just style. None of these objects is shared anywhere until the process is complete and none of them is actually immutable. The code could just as easily use push to add objects to the group arrays and use acc[key] = updatedGroup; to add/update groups to the accumulator object. But again, while it's pointless in this code, there are good uses for immutable programming. Functional programming usually adheres to immutability (as I understand it; I haven't studied FP deeply).
Related
I need to process files that are structured like
Title
/foo/bar/foo/bar 1
/foo/bar/bar 2
/bar/foo 3
/bar/foo/bar 4
It's easy enough to parse this into an array of arrays, by splitting at every / and \n. However, once I get an array of arrays, I can't figure out a good way to turn that into nested objects. Desired format:
{
Title,
{
foo: {
bar: {
foo: {
bar: 1
},
bar: 2
}
},
bar: {
foo: {
3,
bar: 4
}
}
}
}
This seems like a super common thing to do, so I'm totally stumped as to why I can't find any pre-existing solutions. I sort of expected javascript to even have a native function for this, but merging objects apparently overrides values instead of making a nested object, by default. I've tried the following, making use of jQuery.extend(), but it doesn't actually combine like-terms (i.e. parents and grand-parents).
let array = fileContents.split('\n');
let object = array.map(function(string) {
const result = string.split('/').reduceRight((all, item) => ({
[item]: all
}), {});
return result;
});
output = $.extend(true, object);
console.log(JSON.stringify(output));
This turned the array of arrays into nested objects, but didn't merge like-terms...
I could brute-force this, but my real-world problem has over 2000 lines, goes 5 layers deep (/foo/foo/foo/foo value value value), and actually has an array of space-separated values rather than a single value per line. I'm willing to treat the array of values like a string and just pretend its not an array, but it would be really nice to at least get the objects nested properly without writing a hefty/primitive algorithm.
Since this is essentially how subdirectories are organized, it seems like this should be an easy one. Is there a relatively simple solution I'm not seeing?
You could reduce the array of keys/value and set the value by using all keys.
If no key is supplied, it take the keys instead.
const
setValue = (target, keys, value) => {
const last = keys.pop();
keys.reduce((o, k) => o[k] ??= {}, target)[last] = value;
return target;
},
data = 'Title\n/foo/bar/foo/bar 1\n/foo/bar/bar 2\n/bar/foo 3\n/bar/foo/bar 4',
result = data
.split(/\n/)
.reduce((r, line) => {
const [keys, value] = line.split(' ');
return setValue(r, keys.split('/').filter(Boolean), value || keys);
}, {});
console.log(result);
.as-console-wrapper { max-height: 100% !important; top: 0; }
Why does the copy of an array using the spread operator when run through map modify the original array?
What should i do here to not mutate the original array?
const data = {hello : "10"};
const prop = [{name : "hello", color: "red", value : ""}]
const copyProp = [ ...prop ]
copyProp.map(el => {
el.value = data[el.name] || ""
return el
}) //
console.log(copyProp === prop) // -> false
console.log(copyProp) // -> value: 10
console.log(prop) // -> Value: 10 (Should still be "")
The spread operator creates shallow copy of the array. In other words, you create a new array with references to the same objects. So when you modify those objects, the changes are reflected in the original array.
In general, when you need copy an array, you should consider making a deep copy. However, in this case, you just need to use map() correctly. map() creates a new array, so it can make the modified copy for you directly:
const copyProps = props.map(el => {
return {
...el,
value: data[el.name] || '',
}
});
Here I copy each object using the spread operator. This means the resulting array has its own references of objects. This has the same caveat as your original solution: this is a shallow copy. For your example data, it is fine because all values and keys are strings. However, if your real data is more deeply nested with more arrays and objects, you will encounter the same problem.
Both arrays and objects are passed by reference, so when you spread an array you create new array but fill it with references to original objects and when you modify those objects in the new array you still modify the same data in memory that is referenced in both arrays.
Also map method will always return new array so in this case you only need to clone objects and since here you do not have deeply nested objects you can use object spread syntax.
const data = {
hello: "10"
};
const prop = [{
name: "hello",
color: "red",
value: ""
}]
const copyProp = prop.map(el => ({ ...el,
value: data[el.name] || ''
}))
console.log(copyProp)
console.log(prop)
I want to build an object using reduce in this way:
const result = [1, 2].reduce((partialResult, actualValue) => {
// define someKey and someValue
partialResult[someKey] = someValue;
return partialResult
}, {});
However, I'm getting the following tslint error:
Modifying properties of existing object not allowed. (no-object-mutation)
How can I solve this?
Either modify the code so that you're not mutating the object, which means you'll have to copy it each time:
const result = [1, 2].reduce((partialResult, actualValue) => {
return {
...partialResult,
[someKey]: someValue,
}
}, {});
Or disable the lint rule (either for this specific line if you like the lint rule in general, or globally if you think it's an unnecessary restriction)
For example:
const result = [1, 2].reduce(
(partialResult, actualValue) => Object.assign({}, partialResult, {[someKey]: someValue]}), {});
You can use map and Object.fromEntries to create the object all-at-once (as far as your code is concerned) rather than mutating or copying the object:
const someKey = "foo";
const result = Object.fromEntries(
[1, 2].map(value => [someKey + value, value])
// ^^^^^^^^^^^^^^^---- see my comment on the question and
// note below
);
console.log(result);
fromEntries creates an object from an array of entries, where each entry is a [key, value] array.
Note that I've used someKey + value as the key because you haven't said how you get the key, and I assume you're not just using the same key every time, since as I commented here, if you were you'd be replacing the property value every time if you did and end up with an object with a single property, making the entire loop pointless.
I have two APIs to work with and they can't be changed. One of them returns type like this:
{
type: 25
}
and to other API I should send type like this:
{
type: 'Computers'
}
where 25 == 'Computers'. What I want to have is a map of numeric indices to the string value like this:
{
'1': 'Food',
'2': 'Something',
....
'25': 'Computers'
....
}
I am not sure why, but it doesn't feel right to have such map with numeric value to string, but maybe it is completely fine? I tried to Google the answer, but couldn't find anything specific. In one place it says that it is fine, in another some people say that it's better not to have numeric values as object keys. So, who is right and why? Could somebody help me with this question?
Thanks :)
There's nothing wrong with it, but I can understand how it might look a little hinky. One alternative is to have an array of objects each with their own id that you can then filter/find on:
const arr = [ { id: 1, label: 'Food' }, { id: 2, label: 'Something' }, { id: 25, label: 'Computers' } ];
const id = 25;
function getLabel(arr, id) {
return arr.find(obj => obj.id === id).label;
}
console.log(getLabel(arr, id));
You can use the Map object for this if using regular object feels "weird".
const map = new Map()
map.set(25, 'Computers');
map.set(1, 'Food');
// then later
const computers = map.get(25);
// or loop over the map with
map.forEach((id, category) => {
console.log(id, category);
});
Quick Update:
As mentioned by others, using objects with key=value pairs is OK.
In the end, everything in javascript is an object(including arrays)
Using key-value pairs or Map has 1 big advantage( in some cases it makes a huge difference ), and that is having an "indexed" data structure. You don't have to search the entire array to find what you are looking for.
const a = data[id];
is nearly instant, whereas if you search for an id in an array of objects...it all depends on your search algorithm and the size of the array.
Using an "indexed" object over an array gives much better performance if dealing with large arrays that are constantly being updated/searched by some render-loop function.
Map has the advantage of maintaining the insertion order of key-value pairs and it also only iterates over the properties that you have set. When looping over object properties, you have to check that the property belongs to that object and is not "inherited" through prototype chain( hasOwnProperty)
m = new Map()
m.set(5, 'five');
m.set(1, 'one');
m.set(2, 'two');
// some other function altered the same object
m.__proto__.test = "test";
m.forEach((id, category) => {
console.log(id, category);
});
/*
outputs:
five 5
one 1
two 2
*/
o = {};
o[5] = 'five';
o[1] = 'one';
o[2] = 'two';
// something else in the code used the same object and added a new property
// which you are not aware of.
o.__proto__.someUnexpectedFunction = () => {}
for (key in o) {
console.log(key, o[key]);
}
/*
Output:
1 one
2 two
5 five
someUnexpectedFunction () => {}
*/
Map and objects also have 1 very important advantage(sometimes disadvantage - depending on your needs ). Maps/objects/Sets guarantee that your indexed values are unique. This will automatically remove any duplicates from your result set.
With arrays you would need to check every time if an element is already in the array or not.
I'm trying to duplicate key-value pairs from one object into each distinct object inside an array.
const propsToDuplicate = {
foo: 'foo',
bar: 'bar'
};
const items = [{
num: 1
},
{
num: 2
}
];
const result = items.map(item => {
console.log('item: ', item);
const moar = Object.assign(propsToDuplicate, item);
console.log('item with more stuff: ', moar);
return moar;
});
console.log(result);
Questions:
Why do I end up with two instances of the object with num = 2?
How can I perform this operation so that the final result is as below?
Desired result:
[ { foo: 'foo', bar: 'bar', num: 1 },
{ foo: 'foo', bar: 'bar', num: 2 } ]
Here is the sandbox:
https://repl.it/#montrealist/array-map-and-object-assign-weirdness
Object.assign(a, b, c) will assign everything from a, b, and c into the object that is the first parameter to assign().
In your case you are using propsToDuplicate as the first parameter, and this means that that each time assign() is called, propsToDuplicate is being mutated.
Change const moar = Object.assign(propsToDuplicate, item); to const moar = Object.assign({}, propsToDuplicate, item); and you should be good to go.
Why do I end up with two instances of the object with num = 2
because moar === propsToDuplicate in your code. You've assigned all these properties/values to the very same object. And all indices of that array reference the same object.
How can I perform this operation so that the final result is as below?
assign the properties to an empty object:
const morar = Object.assign({}, propsToDuplicate, item);
The workaround is to return JSON.parse(JSON.stringify(moar));
this doesn't really work, as you still assign all the properties into the same object, but you return a snapshot of each iteration. If you add another property only to the first object in items you'll see it show up for the other(later) items as well.
Plus JSON.parse(JSON.stringify(...)) is a ugly approach to clone an object.
Or I could simply flip the parameters - would this work? Object.assign(item, propsToDuplicate);
yes/no/kind of/better not ... I have to explain.
In that case, you'll overwrite the properties in item with the properties in propsToDuplicate, and you'd mutate the objects in the items array. In your current code this would make no difference, but if some item would have a foo or bar property or if propsToDuplicate would contain a num, you'd overwrite that property in the result.
explaining that: Object.assign() is often used to compose some config object with some default values.
let options = Object.assign({}, defaults, config);
this will take an empty Object, then first write all the default values into that and then overwrite soem of the default values with the values passed in the config.
whereas
let options = Object.assign(config, defaults);
will overwrite all the custom configutations with the default values.
Then there's the problem of mutation. The problem with mutation is that depending where you got the object from, and where else it is referenced, you changing the object may introduce errors at the other end of your application, in some completely unrelated peice of code. And then have fun debugging that and finding the error.