Array.prototype for .NET developers
posted in javascript on • by Wouter Van Schandevijl • last updated onThe Array.prototype
functions have been available for a long time but
it’s since Arrow functions
that “Linq lambda syntax”-like functionality is available in JavaScript.
This blog post explains the most common functions by comparing them to their C# equivalents.
A basic example:
// JavaScript
const result = [0, 1, 2, 3, null]
.filter(x => x !== null)
.map(x => x * 10)
.sort((a, b) => b - a);
expect(result).toEqual([30, 20, 10, 0]);
// C#
var result = new int?[] {0, 1, 2, 3, null}
.Where(x => x != null)
.Select(x => x * 10)
.OrderByDescending(x => x)
.ToArray();
Whenever you want to write a for
to loop over an array, the Array.prototype
functions
probably allow you to achieve the same result but in more functional and succinct manner.
Do note that the deferred execution we know from Linq does not apply to Array.prototype
!
Comparison Table
C# | JavaScript | MDN Link |
---|---|---|
Select() | map((cur, index, array): any) | map |
Where() | filter((cur): boolean) | filter |
Contains() | includes(value, fromIndex) | includes |
FirstOrDefault() | find((cur): boolean): undefined | any | find / findIndex |
LastOrDefault() | findLast((cur): boolean): undefined | any | findLast / findLastIndex |
All() | every((cur): boolean): boolean | every |
Any() | some((cur): boolean): boolean | some |
Concat() | concat(arr: any[]): any[] | concat |
Skip(start).Take(start - end) | slice(start = 0, end = length-1) | slice |
string.Join() | join(separator = ‘,’) | join |
Array.IndexOf() | findIndex((cur): boolean): -1 | number | findIndex |
ElementAt() | at(index) (accepts negative indexes) | at |
Count() | length: number | length |
SelectMany() | flat(levels = 1) / flatMap(fn) | flat / flatMap |
GroupBy() | Object.groupBy(arr, groupFn) | groupBy |
Distinct() | filter((el, i, arr) => arr.indexOf(el) === i) | |
Extension method | forEach((cur): void): void | forEach |
Mutating in JS | These are in place operations | |
OrderBy() | sort((a, b): number) | sort / toSorted |
Reverse() | reverse() | reverse / toReversed |
Commonly used methods
Select = map
// C#
var result = enumerable.Select(x => x);
// JavaScript
let result = array.map(x => x);
// Using spread to avoid mutation
const input = [{k: 1, v: true}, {k: 2, v: false}];
const result = input.map(x => ({...x, v: !x.v}))
When mapping to an object without code block, you need to wrap your object between extra parentheses like
[0, 1].map(x => ({value: x}));
// Because without the extra parentheses
[0, 1].map(x => {value: x});
// --> [undefined, undefined]
// it is the same as writing:
[0, 1].map(x => {
value: x; // No error, because JavaScript?
return undefined;
});
Where, Distinct = filter
Where
and filter
behave pretty much exactly alike.
Linqs Distinct
we’ll need to implement ourselves.
const input = [0, 0, 1, 5, 5];
// Equals true when the first occurrence of the value is the current value
const result = input.filter((element, index, array) => array.indexOf(element) === index);
// Or: [...new Set(input)];
expect(result).toEqual([0, 1, 5]);
Aggregate, GroupBy, … = reduce
Linq has Sum, Min, Max, Average, GroupBy, etc.
While JavaScript doesn’t have them, they can all be achieved trivially with reduce
Sum, Min, Max, Average:
const input = [0, 1];
const sum = input.reduce((total, cur) => total + cur, 0);
const average = sum / input.length;
const min = Math.min.apply(Math, input); // Old school
const min = Math.min(...input); // Using spread
Aggregate, GroupBy:
const input = [0, 1, 2, 3];
const result = input.reduce((acc, cur) => {
if (cur % 2 === 0) {
acc.even.push(cur);
} else {
acc.odd.push(cur);
}
return acc;
}, {even: [], odd: []});
expect(result).toEqual({even: [0, 2], odd: [1, 3]});
Since 2024, JS has a dedicated Object.groupBy
:
const socks = [
{ name: 'JavaScript', type: 'lang' },
{ name: 'Angular', type: 'package' },
{ name: 'React', type: 'package' },
];
const grouped = Object.groupBy(socks, sock => sock.type);
expect(grouped).toEqual({
lang: [{ name: 'JavaScript', type: 'lang' }],
package: [
{ name: 'Angular', type: 'package' },
{ name: 'React', type: 'package' },
]
})
slice
Linq has First, Last, Skip, SkipLast, SkipWhile, Take, TakeLast, TakeWhile.
JavaScript has slice
.
// Shallow copy
const input = [0, 1, 2, 3];
expect(input.slice()).toEqual([...input]);
// Signature
slice(startIndex = 0, endIndex = length-1);
C# Ranges
var array = new[] { 1, 2, 3, 4, 5 };
Assert.Equal(new[] { 3, 4 }, array[2..^1]);
Assert.Equal(new[] { 1, 2 }, array[..^3]);
Assert.Equal(new[] { 3, 4, 5 }, array[2..]);
Assert.Equal(new[] { 1, 2, 3, 4, 5 }, array[..]);
with
Since 2023, there is also with
which returns a new
array where one item is replaced with a new value by index.
const newArr = arr.with(index, newValue);
Mutations
These functions operate in place.
Since ECMAScript 2023, some have non-mutating alternatives!
OrderBy = sort and toSorted
- Use toSorted if you need a new array for the sorted result.
- Without compareFn the array is sorted according to each character’s Unicode code point value.
- The compareFn should return a number:
-1
(or any negative number):a
comes beforeb
0
: Equal1
(or any positive number):a
comes afterb
// Numbers
[10, 5].sort(); // [10, 5]: each element is converted to a string
[10, 5].sort((a, b) => a - b); // [5, 10]
// Strings
['z', 'e', 'é'].sort(); // ['e', 'z', 'é']
['z', 'e', 'é'].sort((a, b) => a.localeCompare(b)); // ['e', 'é', 'z']
// Dates
[d1, d2, d3].sort((a, b) => a.getTime() - b.getTime());
C#
- The signature is a bit different:
OrderBy(Func<TSource, TKey> keySelector, IComparer<TKey> comparer)
OrderBy
vsOrderByDescending
: switcha
andb
…- JS doesn’t have
ThenBy
chaining possibilities, you’ll have to do this yourself.
forEach
While forEach
is not really doing any mutation by itself, it is often what it’s used for.
Mutations are especially dangerous in for example a Redux environment where UI changes might lag.
The same can usually be achieved with map
.
const input = [{value: 1, visited: 0}, {value: 2, visited: 0}];
// Potentially dangerous
input.forEach(el => {
el.visited++;
});
// ...el will create a shallow copy only!
const result = input.map(el => ({...el, visited: el.visited + 1}));
// Or use the good old loop?
for (let itm of input) {
console.log(itm.value);
}
Other mutators
reverse()
: There is now alsotoReversed()
(2023)push(el1, [el2, ...])
: Add element(s) at the end. Returns the new array length.shift()
: Remove the first element at the start. Returns the removed element.unshift(el, [el2, ...])
: Add element(s) at the start. Returns the new array length.splice(start, [deleteCount, [el1, [el2, ...]]])
- The swiss army knife: add and/or remove element(s)
- Returns array of removed elements (or empty array)
- There is now also toSpliced (2023)
Linq for ages 5 and up
Thanks Wim De Cleen (and Martin Fowler) for this amazing diagram, I love it!
- msn0/mdn-polyfills : Include polyfills for your favourite functions that are not (yet) implemented by your browser(s).
- 2ality.com: Blogging on ES proposals
- es6-features.org: Overview and Comparison
- StackOverflow: Jon Skeet on why there is no forEach in Linq
- morelinq/MoreLINQ : Extensions to LINQ to Objects
- MooTools: Why Contains is called includes in JavaScript
- 1 June 2024 : Added Object.groupBy
- 6 October 2023 : More updates for ECMAScript 2023
- 4 September 2023 : Added flat and flatMap
- 8 March 2023 : Added Distinct, flatMap