Related
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).
In its most basic form, having an array of objects:
let arr = [
{val:"a"},
{val:"b"}
];
How can destructuring be used, to obtain only the values ['a', 'b'].
getting the first value is easy:
let [{val:res}] = arr; //res contains 'a'
Obtaining all values inside the array can be done with the rest operator:
let [...res] = arr; //res contains all objects
Combining those, I expected to be able to use:
let [...{val:res}] = arr; //undefined, expected all 'val's (['a', 'b'])
The above returns undefined (Tested in FF). Some further testing seems to indicate that adding the rest operator when using an object destructuring as well doesn't use the iteration, but gets back the original object, e.g. let [...{length:res}] = arr; //res= 2. Some other trials, such as let [{val:...res}] = arr; or let [{val}:...res] = arr; produce syntax errors.
It's easy enough to do with other methods, such as using map on the array, but mostly I stumble upon this problem while destructuring multiple levels (an array with objects which have their own property containing an array). Therefore I'm really trying to get around how to do it solely with destructuring.
For convenience: a test fiddle
edit
My apologies if I failed to explain the goal of the question. I'm not looking for a solution to a specific problem, only to find the correct syntax to use when destructuring.
Otherwise formulated, a first question would be: in the example above, why doesn't let [...{val:res}] = arr; return all values (['a', 'b']). The second question would be: what is the proper syntax to use a rest operator with a nested object destructuring? (pretty sure I've gotten some definitions mixed up here). It seems that the latter is not supported, but I haven't come across any documentation that (and why) it wouldn't be.
Why doesn't let [...{val:res}] = arr; return all values (['a', 'b'])?
You seem to confuse the rest syntax with array comprehensions.
If you assign a value to [someElements, ...someExpression], the value is tested to be iterable and then each element generated by the iterator is assigned to the respective someElements variable. If you use the rest syntax in the destructuring expression, an array is created and the iterator is ran till its end while filling the array with the generated values. Then that array is assigned to the someExpression.
All of these assignment targets can be other destructuring expressions (arbitrarily nested and recursively evaluated), or references to variable or properties.
So if you do let [...{val:res}] = arr, it will create an array and fill that with all the values from the iterator of arr:
let {val:res} = Array.from(arr[Symbol.iterator]())
You can see now why that ends up with undefined, and why using something like [...{length:res}] does yield a result. Another example:
let [{val:res1}, ...{length: res2}] = arr;
console.log(res1) // 'a'
console.log(res2) // 1 (length of `[{val: 'b'}]`)
How can destructuring be used to obtain only the values ['a', 'b']?
Not at all. Use the map method.
You can destructure nested objects like this
https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Operators/Destructuring_assignment#Nested_object_and_array_destructuring
let arr = [
{val:"a"},
{val:"b"}
];
const [{val: valueOfA}, {val: valueOfB}] = arr
console.log(
valueOfA, valueOfB
)
Beside mapping with a callback for the value
let arr = [{ val: "a" }, { val: "b" }];
console.log(arr.map(o => o.val));
you could use deconstructiong inside of the paramter list and use only the value to return.
let arr = [{ val: "a" }, { val: "b" }];
console.log(arr.map(({val}) => val));
At this point of time you can use both For of loop with ES6 Object destructuring.
let arr = [{val:"a"},{val:"b"}];
for (const item in arr){
const {val} = arr[item];
console.log(val);
}
You can declare assignment target before destructuring assignment; at destructuring target, set values of assignments target indexes by from destructuring source
let arr1 = [{val: "a"}, {val: "b"}];
let arr2 = [{"foo":1,"arr":[{"val":"a"},{"val":"b"}]}
, {"foo":2,"arr":[{"val":"c"},{"val":"d"}]}];
let [res1, res2] = [[], []];
[{val: res1[0]}, {val: res1[1]}] = arr1;
[{arr: [{val:res2[0]}, {val:res2[1]}]}
, {arr: [{val:res2[2]}, {val:res2[3]}]}] = arr2;
console.log(res1, res2);
You can alternatively use rest element at target to collect values at source by including comma operator following object pattern to return value pulled from object
let arr = [{val: "a"}, {val: "b"}];
let [...res] = [({val} = arr[0], val), ({val} = arr[1], val)];
console.log(res)
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.
In its most basic form, having an array of objects:
let arr = [
{val:"a"},
{val:"b"}
];
How can destructuring be used, to obtain only the values ['a', 'b'].
getting the first value is easy:
let [{val:res}] = arr; //res contains 'a'
Obtaining all values inside the array can be done with the rest operator:
let [...res] = arr; //res contains all objects
Combining those, I expected to be able to use:
let [...{val:res}] = arr; //undefined, expected all 'val's (['a', 'b'])
The above returns undefined (Tested in FF). Some further testing seems to indicate that adding the rest operator when using an object destructuring as well doesn't use the iteration, but gets back the original object, e.g. let [...{length:res}] = arr; //res= 2. Some other trials, such as let [{val:...res}] = arr; or let [{val}:...res] = arr; produce syntax errors.
It's easy enough to do with other methods, such as using map on the array, but mostly I stumble upon this problem while destructuring multiple levels (an array with objects which have their own property containing an array). Therefore I'm really trying to get around how to do it solely with destructuring.
For convenience: a test fiddle
edit
My apologies if I failed to explain the goal of the question. I'm not looking for a solution to a specific problem, only to find the correct syntax to use when destructuring.
Otherwise formulated, a first question would be: in the example above, why doesn't let [...{val:res}] = arr; return all values (['a', 'b']). The second question would be: what is the proper syntax to use a rest operator with a nested object destructuring? (pretty sure I've gotten some definitions mixed up here). It seems that the latter is not supported, but I haven't come across any documentation that (and why) it wouldn't be.
Why doesn't let [...{val:res}] = arr; return all values (['a', 'b'])?
You seem to confuse the rest syntax with array comprehensions.
If you assign a value to [someElements, ...someExpression], the value is tested to be iterable and then each element generated by the iterator is assigned to the respective someElements variable. If you use the rest syntax in the destructuring expression, an array is created and the iterator is ran till its end while filling the array with the generated values. Then that array is assigned to the someExpression.
All of these assignment targets can be other destructuring expressions (arbitrarily nested and recursively evaluated), or references to variable or properties.
So if you do let [...{val:res}] = arr, it will create an array and fill that with all the values from the iterator of arr:
let {val:res} = Array.from(arr[Symbol.iterator]())
You can see now why that ends up with undefined, and why using something like [...{length:res}] does yield a result. Another example:
let [{val:res1}, ...{length: res2}] = arr;
console.log(res1) // 'a'
console.log(res2) // 1 (length of `[{val: 'b'}]`)
How can destructuring be used to obtain only the values ['a', 'b']?
Not at all. Use the map method.
You can destructure nested objects like this
https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Operators/Destructuring_assignment#Nested_object_and_array_destructuring
let arr = [
{val:"a"},
{val:"b"}
];
const [{val: valueOfA}, {val: valueOfB}] = arr
console.log(
valueOfA, valueOfB
)
Beside mapping with a callback for the value
let arr = [{ val: "a" }, { val: "b" }];
console.log(arr.map(o => o.val));
you could use deconstructiong inside of the paramter list and use only the value to return.
let arr = [{ val: "a" }, { val: "b" }];
console.log(arr.map(({val}) => val));
At this point of time you can use both For of loop with ES6 Object destructuring.
let arr = [{val:"a"},{val:"b"}];
for (const item in arr){
const {val} = arr[item];
console.log(val);
}
You can declare assignment target before destructuring assignment; at destructuring target, set values of assignments target indexes by from destructuring source
let arr1 = [{val: "a"}, {val: "b"}];
let arr2 = [{"foo":1,"arr":[{"val":"a"},{"val":"b"}]}
, {"foo":2,"arr":[{"val":"c"},{"val":"d"}]}];
let [res1, res2] = [[], []];
[{val: res1[0]}, {val: res1[1]}] = arr1;
[{arr: [{val:res2[0]}, {val:res2[1]}]}
, {arr: [{val:res2[2]}, {val:res2[3]}]}] = arr2;
console.log(res1, res2);
You can alternatively use rest element at target to collect values at source by including comma operator following object pattern to return value pulled from object
let arr = [{val: "a"}, {val: "b"}];
let [...res] = [({val} = arr[0], val), ({val} = arr[1], val)];
console.log(res)
Are there any substantial reasons why modifying Array.push() to return the object pushed rather than the length of the new array might be a bad idea?
I don't know if this has already been proposed or asked before; Google searches returned only a myriad number of questions related to the current functionality of Array.push().
Here's an example implementation of this functionality, feel free to correct it:
;(function() {
var _push = Array.prototype.push;
Array.prototype.push = function() {
return this[_push.apply(this, arguments) - 1];
}
}());
You would then be able to do something like this:
var someArray = [],
value = "hello world";
function someFunction(value, obj) {
obj["someKey"] = value;
}
someFunction(value, someArray.push({}));
Where someFunction modifies the object passed in as the second parameter, for example. Now the contents of someArray are [{"someKey": "hello world"}].
Are there any drawbacks to this approach?
See my detailed answer here
TLDR;
You can get the return value of the mutated array, when you instead add an element using array.concat[].
concat is a way of "adding" or "joining" two arrays together. The awesome thing about this method, is that it has a return value of the resultant array, so it can be chained.
newArray = oldArray.concat[newItem];
This also allows you to chain functions together
updatedArray = oldArray.filter((item) => {
item.id !== updatedItem.id).concat[updatedItem]};
Where item = {id: someID, value: someUpdatedValue}
The main thing to notice is, that you need to pass an array to concat.
So make sure that you put your value to be "pushed" inside a couple of square brackets, and you're good to go.
This will give you the functionality you expected from push()
You can use the + operator to "add" two arrays together, or by passing the arrays to join as parameters to concat().
let arrayAB = arrayA + arrayB;
let arrayCD = concat(arrayC, arrayD);
Note that by using the concat method, you can take advantage of "chaining" commands before and after concat.
Are there any substantial reasons why modifying Array.push() to return the object pushed rather than the length of the new array might be a bad idea?
Of course there is one: Other code will expect Array::push to behave as defined in the specification, i.e. to return the new length. And other developers will find your code incomprehensible if you did redefine builtin functions to behave unexpectedly.
At least choose a different name for the method.
You would then be able to do something like this: someFunction(value, someArray.push({}));
Uh, what? Yeah, my second point already strikes :-)
However, even if you didn't use push this does not get across what you want to do. The composition that you should express is "add an object which consist of a key and a value to an array". With a more functional style, let someFunction return this object, and you can write
var someArray = [],
value = "hello world";
function someFunction(value, obj) {
obj["someKey"] = value;
return obj;
}
someArray.push(someFunction(value, {}));
Just as a historical note -- There was an older version of JavaScript -- JavaScript version 1.2 -- that handled a number of array functions quite differently.
In particular to this question, Array.push did return the item, not the length of the array.
That said, 1.2 has been not been used for decades now -- but some very old references might still refer to this behavior.
http://web.archive.org/web/20010408055419/developer.netscape.com/docs/manuals/communicator/jsguide/js1_2.htm
By the coming of ES6, it is recommended to extend array class in the proper way , then , override push method :
class XArray extends Array {
push() {
super.push(...arguments);
return (arguments.length === 1) ? arguments[0] : arguments;
}
}
//---- Application
let list = [1, 3, 7,5];
list = new XArray(...list);
console.log(
'Push one item : ',list.push(4)
);
console.log(
'Push multi-items :', list.push(-9, 2)
);
console.log(
'Check length :' , list.length
)
Method push() returns the last element added, which makes it very inconvenient when creating short functions/reducers. Also, push() - is a rather archaic stuff in JS. On ahother hand we have spread operator [...] which is faster and does what you needs: it exactly returns an array.
// to concat arrays
const a = [1,2,3];
const b = [...a, 4, 5];
console.log(b) // [1, 2, 3, 4, 5];
// to concat and get a length
const arrA = [1,2,3,4,5];
const arrB = [6,7,8];
console.log([0, ...arrA, ...arrB, 9].length); // 10
// to reduce
const arr = ["red", "green", "blue"];
const liArr = arr.reduce( (acc,cur) => [...acc, `<li style='color:${cur}'>${cur}</li>`],[]);
console.log(liArr);
//[ "<li style='color:red'>red</li>",
//"<li style='color:green'>green</li>",
//"<li style='color:blue'>blue</li>" ]
var arr = [];
var element = Math.random();
assert(element === arr[arr.push(element)-1]);
How about doing someArray[someArray.length]={} instead of someArray.push({})? The value of an assignment is the value being assigned.
var someArray = [],
value = "hello world";
function someFunction(value, obj) {
obj["someKey"] = value;
}
someFunction(value, someArray[someArray.length]={});
console.log(someArray)