Array.prototype for .NET developers

Array.prototype for .NET developers

posted in javascript on

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!

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
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

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]

// 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. 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]);

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]});

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);

Mutations

These functions operate in place.

OrderBy = sort

  • 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 before b
    • 0: Equal
    • 1 (or any positive number): a comes after b
// 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 vs OrderByDescending: switch a and b

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


Stuff that came into being during the making of this post
Other interesting reads
Tags: cheat-sheet tutorial