Javascripts gives us lots of methods to perform operations on arrays in functional style, such as filter, map, reduce etc.
const input=[
{name: "John", points: 5},
{name: "Bob", points: 0},
{name: "Alice", points: 4}];
console.log(input.filter(x=>x.points>0).map(x=>x.name));
// => ["John", "Alice"]
But capabilities of this built-in methods are not so powerful like for example in C# and .Net.
There are libraries designed for this purpose. For example Lodash has a lot of functions:
var _ = require('lodash');
const input=[
{name: "John", points: 5},
{name: "Bob", points: 0},
{name: "Alice", points: 4}];
console.log(_.map(_.filter(input, x=>x.points>0),x=>x.name));
// => ["John", "Alice"]
As you see, this method are not invoked on arras but gets arrays as a parameter. If you want to chain methods (like in example, filter and there map) code starts to be less readable – name of method map is on beginning of line, but it’s method – on end.
Extensions
In C# there is something called ‘extension method’. This is method, that is added to class outside it.
public static class A{
public static string MyMethod(this List<string> a){
return "Hello, world!";
}
}
//...
var animals = new List<string>{"cat", "dog", "lion"};
Console.WriteLine(animals.MyMethod());
You can do very similar think in JS. Javascript uses prototype-based inheritance, so all arrays (not every array-like object is array) inherit from Array.prototype. It is object built-in browser, but you can add to it properties and methods like to any other objects.
const animals = ["cat", "dog", "lion"];
Array.prototype.myMethod=function(){return "Hello, world!";}
console.log(animals.myMethod());// => "Hello, world!"
Now many experienced JS developers probably treats me like heretic. It is because main difference between this and C# extension method is that in C# extension methods exist in some namespace and it is visible only after importing this namespace. If you edit prototype in JS, you do it globally (not only for current scope, but for all arrays). It is called prototype pollution.
So be very careful with it. If you have project written by more than you – please inform other programmers what you doing. And I advise against doing this in libraries.
What methods you can add?
I will show you some examples of useful methods you can add. If you want, you can download npm library I created (but I encourage you to write yourself)
npm i prototype-extensions
# OR
yarn add prototype-extensions
sum
First of methods simply returns one number, that is sum of all items.
const array=[10, 2, 5, 0.5];
console.log(array.sum());// => 17.5
Sometimes we don’t have plain numbers, but complex objects. That is why a good idea it pass as argument function, that will extract plain number for us.
const work=[
{worket: 'Alice', workedHours:8, pricePerHour:20},
{worket: 'Bob', workedHours:10, pricePerHour:15},
{worket: 'John', workedHours:2, pricePerHour:30},
];
const totalCost = work.sum(x=>x.workedHours * pricePerHour);
console.log(totalCost);// => 370
Implementation
if (!Array.prototype.sum) {
Array.prototype.sum = function (fun = x => x) {
return this.reduce((sum, item) => sum + Number(fun(item)), 0);
}
}
First if is to ensure, that we don’t override any already existing method.
At second line we create new function and assign to prototype of arrays. As parameter we take other function, that we will execute on each array of item. Default value of this parameter is function, that do nothing. If this syntax is unclear for you, here is other version that do the same
Array.prototype.sum = function (fun) {
if(!fun){
fun = function(input){
return input;
}
}
sortBy
It is very common think to sort. But in built-in js method require function that compare 2 objects and return if first is bigger, smaller or equal. It will be much more convenient to just sort by value (or multiple values)
const people=[
{name:'Anna', age:18},
{name:'Bob', age:25},
{name:'John', age:17},
];
people.sortBy(x=>x.age);
console.log(people);// => [
// => {name:'John', age:17},
// => {name:'Anna', age:18},
// => {name:'Bob', age:25},
// => ];
Implementation
if (!Array.prototype.sortBy) {
Array.prototype.sortBy = function (...args) {
let orders = args.map(x => typeof x == 'function' ? x : y => y[x]);
let compareFunction = (a, b) => {
for (let order of orders) {
let valueA = order(a);
let valueB = order(b);
if (valueA > valueB)
return 1;
else if (valueA < valueB)
return -1;
}
return 0;
};
return this.sort(compareFunction);
}
}
I assumed, that you could want to order by many values (if some values were equal sort by next). That’s why I used …args notation (it creates an array of function’s arguments) and used for..of loop.
I also assumed, that you could as argument put not only method, but also string with name of property (that is why I put map function)
min & max
How to find biggest or lowest element of array? You could sort it, and then get first/last element, but this solution has one problem – performance. it’s much much quicker to make method special for finding one minimum/maximum element.
const array1 = [5,10,2.2,-30];
console.log(array1.min()); // => -30
console.log(array1.max()); // => 10
But in objective world, it’s rare to have arrays of plain numbers.
const people=[
{name:'Anna', age:18},
{name:'Bob', age:25},
{name:'John', age:17},
];
console.log(people.min(x=>x.age));// => {name:'John', age:17}
Implementation
if (!Array.prototype.max) {
Array.prototype.max = function (fun = x => x) {
let value = null;
let object = null;
for (let item of this) {
const itemValue = fun(item);
if (typeof itemValue === 'number' && !isNaN(itemValue) && (value == null || itemValue > value)) {
value = itemValue;
object = item;
}
}
return object;
}
}
groupBy
Sometimes you want to group some objects by common property:
const people=[
{name:'Alice', type:'teacher'},
{name:'Bob', type:'student'},
{name:'John', type:'student'}
];
const grouped = people.groupBy(x => x.type);
console.log(grouped); // => Map(2){
// => "teacher" => [{name:'Alice', type:'teacher'}],
// => "student" => [{name:'Bob', type:'student'}, {name:'John', type:'student'}]
// => }
You can ask, why I return Map instead of simple Object? It’s because in Map key could be everything, not only strings and symbols, so this gives us more flexibility.
Implementation
if (!Array.prototype.groupBy) {
Array.prototype.groupBy = function (fun = x => x) {
const ret = new Map();
for (const value of this) {
const key = fun(value);
if (ret.has(key))
ret.get(key).push(value);
else
ret.set(key, [value]);
}
return ret;
}
}
Future
In ECMAScript Next specification there is proposed bind operator :: ( https://github.com/tc39/proposal-bind-operator) which would be better solution that extending prototypes. But it is on stage 0, which means it isn’t sure that it will be added to JavaScript specification, and even if, it would take lots of time until browsers will implement this.