layout | author | title | date | desc | img | categories | tags | extras | interesting | toc | ||||||||||||||||||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
post |
Wouter Van Schandevijl |
Array.prototype for .NET developers |
2019-02-23 16:00:00 -0800 |
A guide on learning JavaScript `Array.prototype` by comparing it to C# Linq.
|
Array.prototype.png |
javascript |
|
|
|
|
The 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
!
| 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 | 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 | Count() | length: number | length | Extension method | forEach((cur): void): void | forEach | | Mutating in JS | These are in place operations | | OrderBy() | sort((a, b): number) | sort | Reverse() | reverse() | reverse |---------------------------------|------------------------------------------------|---------- {: .table-code}
// 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]
// is the same as writing:
[0, 1].map(x => {
value: x; // No error, because JavaScript?
return undefined;
});
Where
and filter
behave pretty much exactly alike.
Linq's Distinct
we'll need to implement ourselves.
const input = [0, 0, 1, 5, 5];
// Equals true when the first occurence 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]);
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]});
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);
These functions operate in place.
- Do a
slice()
first if you need a different array reference for the result of the sort. - 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
...
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);
}
reverse()
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)