English version
Javascriptowe tablice mają kilka metod, które pozwalają operować na danych wg. podejścia funkcyjnego (np. filter, map, reduce).
const input=[
{name: "John", points: 5},
{name: "Bob", points: 0},
{name: "Alice", points: 4}];
const result = input.filter(x=>x.points>0).map(x=>x.name);
console.log(result);// => ["John", "Alice"]
Przy czym są to tylko podstawowe metody, nie mamy tak dużych możliwości jak np. biblioteka LINQ w C#.
Istnieją biblioteki do JS o większych możliwościach. Przykładem niech będzie Lodash:
var _ = require('lodash');
const input=[
{name: "John", points: 5},
{name: "Bob", points: 0},
{name: "Alice", points: 4}];
const result = _.map(_.filter(input, x=>x.points>0),x=>x.name);
console.log(result);// => ["John", "Alice"]
Lodash daje nam dość dużo metod do wykorzystania, jednak tych metod nie wykonujemy na tablicy, tylko tablicę przekazujemy jako parametr metody. Różnica może niewielka, ale gdy chcemy wywołać metody jedna pod drugiej (jak w przykładach powyżej) kod robi się nieczytelny: nie wykonuje się od lewej do prawej, tylko niejako od środka na zewnątrz.
Extensions
W C# jest coś takiego jak „extension method” (pozwolę sobie nie tłumaczyć na siłę na „metody rozszerzające” jak robi to oficjalna dokumentacja tylko zostanę przy oryginalnej nazwie). Jest to sposób na dodanie metody do klasy bez modyfikacji tej klasy.
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()); // => "Hello, world!"
W JS można zrobić coś bardzo podobnego (ale nie identycznego, o czym później). Javascript wykorzystuje dziedziczenie oparte na prototypach, więc wszystkie tablice to obiekty dziedziczące z obiektu Array.prototype. Jest to obiekt wbudowany w przeglądarkę, ale możesz do niego przypisywać nowe property i metody jak do każdego zwykłego obiektu, a zmiany będą widoczny we wszystkich obiektach które po nim dziedziczą (nawet tych utworzonych wcześniej).
const animals = ["cat", "dog", "lion"];
Array.prototype.myMethod=function(){return "Hello, world!";}
console.log(animals.myMethod());// => "Hello, world!"
I w tym momencie doświadczeni programiści Javascriptu prawdopodobnie uznają mnie za jakiegoś heretyka. W tym momencie warto wspomnieć czym różni się to podejście od extensionów w C#. Otóż extension w C# znajduje się w jakimś namespace i jest widoczny tylko wtedy, gdy ten namespace w danym pliku załadujemy. Za to edycja prototypów w JS działa globalnie (na wszystkie tablice wewnątrz danego window, niezależnie w jakim pliku jesteśmy). Ma to swoją nazwę: zanieczyszczenie prototypów (ang. prototype pollution).
Dlatego stosuj tą technikę ostrożnie. Jeśli piszesz projekt w kilka osób: uzgodnij z całym zespołem jeśli chcesz namieszać w globalnych prototypach. Odradzam też robienie tego gdy piszesz bibliotekę dla kogoś.
Jakie metody można sobie dopisać?
Zaprezentuję tutaj kilka użytecznych metod które warto dopisać, wraz z implementacją i przykładami użycia. Stworzyłem bibliotekę npm-ową która je zawiera (ale zachęcam cię do pisania tego typu metod samodzielnie)
npm i prototype-extensions
# LUB
yarn add prototype-extensions
sum
Pierwsza z metod po prostu dodaje wszystkie elementy tablicy do siebie, zwracając liczbę.
const array=[10, 2, 5, 0.5];
console.log(array.sum());// => 17.5
Często jednak nie mamy gołych liczb, ale całe obiekty. Dlatego wygodnie jest móc przekazać funkcję, która wyłuska nam z obiektu liczbę, którą chcemy.
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
implementacja
if (!Array.prototype.sum) {
Array.prototype.sum = function (fun = x => x) {
return this.reduce((sum, item) => sum + Number(fun(item)), 0);
}
}
Pierwszy if jest żeby się upewnić, że nie nadpisujemy jakiejś już istniejącej metody, którą mógł dodać ktoś inny (lub jeśli pojawi się nowa metoda w samej przeglądarce)
W drugiej linii tworzymy nową funkcję, którą przypisujemy do prototypu tablic (Array.prototype). Jako parametr funkcji przekazujemy funkcję (wiem jak to brzmi, ale trochę na tym polega programowanie funkcyjne) która zamieni nam element tablicy na liczbę. Domyślnym parametrem jest funkcja która zwraca to co przyjęła (bo chcemy, żeby w sytuacji, gdy nie podamy żadnego parametru, były brane elementy tablicy tak jak są).
Jeśli ten zapis jest dla ciebie niezrozumiały oto linia nr 2 w wersji bardziej rozpisanej:
Array.prototype.sum = function (fun) {
if(!fun){
fun = function(input){
return input;
}
}
//...
sortBy
Sortowanie danych to dosyć podstawowa operacja. Jednak wbudowana w JS metoda sort wymaga podania mody, która porównuje dwie zmienne ze sobą i zwraca, która z nich jest mniejsza. Dużo wygodniejsze byłoby sortowanie po konkretnej wartości (lub kilku wartościach), która jest liczbą.
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},
// => ];
Implementacja
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);
}
}
Zakładam, że być może programista chciały posortować po kilku wartościach (jeśli pierwsza wartość jest identyczna posortuj po kolejnej), dlatego użyłem zapisu …args (z parametrów funkcji robi nam tablicę) a następnie pętli for..of.
Dodałem też możliwość, aby nie tylko przekazać parametrem funkcję, ale też nazwę property w formie stringa (dlatego użyłem metody map)
min i max
Jak znaleźć największy lub najmniejszy element tablicy? Można tablicę posortować, a potem wziąć z niej pierwszy/ostatni element, ale to podejście jest dosyć słabe z punktu widzenia wydajności. Dużo wydajniej jest mieć metodę która znajduje jeden element.
const array1 = [5,10,2.2,-30];
console.log(array1.min()); // => -30
console.log(array1.max()); // => 10
W programowaniu obiektowym rzadko spotykamy tablice zawierające same liczby. Raczej mamy całe obiekty, które mają w sobie jakieś wartości liczbowe:
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}
Implementacja
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
Czasem przydaje się pogrupować obiekty po jakiejś wspólnej wartości:
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'}]
// => }
Można spytać, dlaczego użyłem Map zamiast zwykłego Object? Dlatego że w Map kluczem może być wszystko, co daje nam większą elastyczność, gdzie przy zastosowaniu Object kluczem może być tylko String i Symbol.
Implementacja
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;
}
}
Przyszłość
W specyfikacji ECMAScript Next zaproponowano nowy operator :: (https://github.com/tc39/proposal-bind-operator), który byłby dużo lepszym rozwiązaniem niż rozszerzanie prototypów, ale w tym momencie jest określany jako stage 0, czyli nie jest nawet pewne czy i kiedy zostanie dodany do specyfikacji JavaScriptu, a nawet jeśli, minie dużo czasu zanim przeglądarki go zaimplementują.
Linki
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Inheritance_and_the_prototype_chain