Strona główna / Artykuły / Współczesne metody tablic w JavaScript, które zastępują powszechne szablony boilerplate

Współczesne metody tablic w JavaScript, które zastępują powszechne szablony boilerplate

Dowiedz się, jak nowsze metody tablic JavaScript, takie jak groupBy, toSorted, with i findLast, zastępują rozbudowane rozwiązania oparte na reduce, spread i reverse.

1432 słów

Grupowanie danych bez biblioteki: Object.groupBy() i Map.groupBy()

Przez lata dzielenie tablicy elementów na kategorie na podstawie wspólnej właściwości oznaczało konieczność napisania własnego akumulatora reduce() lub użycia lodash.groupBy wyłącznie do tego zadania.

Specyfikacja ECMAScript oferuje teraz wbudowane funkcje grupowania poprzez Object.groupBy() i Map.groupBy().

Jaki problem to rozwiązuje

Gdy pobierasz prostą kolekcję rekordów z API — transakcje, zadania, wpisy użytkowników, cokolwiek — często musisz je uporządkować przed wyświetleniem lub zastosowaniem do nich logiki biznesowej.

Wcześniej: ręczne tworzenie z użyciem reduce()

const inventory = [
  { name: 'Apples', type: 'fruit', quantity: 10 },
  { name: 'Bananas', type: 'fruit', quantity: 0 },
  { name: 'Carrots', type: 'vegetable', quantity: 14 },
  { name: 'Broccoli', type: 'vegetable', quantity: 5 }
];

const groupedByReduce = inventory.reduce((acc, item) => {
  const key = item.type;
  if (!acc[key]) {
    acc[key] = [];
  }
  acc[key].push(item);
  return acc;
}, {});

Później: jasne, specjalnie stworzone wywołanie Object.groupBy

const inventory = [
  { name: 'Apples', type: 'fruit', quantity: 10 },
  { name: 'Bananas', type: 'fruit', quantity: 0 },
  { name: 'Carrots', type: 'vegetable', quantity: 14 },
  { name: 'Broccoli', type: 'vegetable', quantity: 5 }
];

const grouped = Object.groupBy(inventory, item => item.type);

/*
Output:
{
  fruit: [
    { name: 'Apples', type: 'fruit', quantity: 10 },
    { name: 'Bananas', type: 'fruit', quantity: 0 }
  ],
  vegetable: [
    { name: 'Carrots', type: 'vegetable', quantity: 14 },
    { name: 'Broccoli', type: 'vegetable', quantity: 5 }
  ]
}
*/

Zamiast tego użyj Map.groupBy()

Gdy klucz grupowania nie jest zwykłą łańcuchem lub symbolem, lecz bardziej złożonym obiektem lub dynamicznym odniesieniem, Map.groupBy() jest lepszym rozwiązaniem:

const vipTier = { tier: 'VIP' };
const standardTier = { tier: 'Standard' };

const customers = [
  { name: 'Alice', plan: vipTier },
  { name: 'Bob', plan: standardTier },
  { name: 'Charlie', plan: vipTier }
];

const groupedByPlan = Map.groupBy(customers, user => user.plan);

console.log(groupedByPlan.get(vipTier));
// Returns Alice and Charlie's records directly keyed by reference

Niezmiennicze transformacje, którym można zaufać: toSorted(), toReversed(), toSpliced()

Klasycznym problemem w JavaScript jest to, że powszechnie używane metody .sort(), .reverse() oraz .splice() zawsze modyfikują tablicę bezpośrednio, zamiast zwracać nowy obiekt.

W frameworkach opartych na przewidywalnym stanie — React, Redux, Vue i podobnych — bezpośrednia modyfikacja tablicy często powoduje subtelne błędy oraz problem z synchronizacją renderowania.

Jaki problem to rozwiązuje

Aby uniknąć tych pułapek, programiści zwykle najpierw pisali kopie ochronne, takie jak [...array].sort() lub array.slice().reverse(). Nowa grupa metod „zmiana tablicy poprzez kopię” całkowicie eliminuje ten zbędny kod.

Porównanie obu podejść

// --- The Old Mutating Way ---
const scores = [88, 92, 79, 95];
const sortedMutated = scores.sort((a, b) => a - b);

console.log(scores); // [79, 88, 92, 95] -> Original data was destroyed!

// --- The Modern Non-Mutating Way ---
const originalScores = [88, 92, 79, 95];
const cleanSorted = originalScores.toSorted((a, b) => a - b);

console.log(originalScores); // [88, 92, 79, 95] -> Unchanged
console.log(cleanSorted);    // [79, 88, 92, 95] -> Fresh array

Zamiana splice() na toSpliced()

toSpliced() umożliwia usuwanie, wstawianie lub zastępowanie elementów w dowolnej pozycji, nie naruszając przy tym oryginalnej tablicy:

const tabs = ['Home', 'About', 'Pricing', 'Contact'];

// Remove 'Pricing' (index 2) and insert 'Services' & 'Blog'
const updatedTabs = tabs.toSpliced(2, 1, 'Services', 'Blog');

console.log(tabs);        // ['Home', 'About', 'Pricing', 'Contact']
console.log(updatedTabs); // ['Home', 'About', 'Services', 'Blog', 'Contact']

Aktualizacja jednego elementu w sposób niezmienialny: with()

Załóżmy, że chcemy zmienić wartość w określonym indeksie bez ingerencji w tablicę źródłową. Zwykłe podejście polega na utworzeniu kopii tablicy, zapisaniu nowej wartości w danym indeksie i zwróceniu wyniku. with() łączy to wszystko w jedną przejrzystą wyrażenie.

Jaki problem to rozwiązuje

Zaktualizowanie tablicy często wymaga zamiany pojedynczego elementu na podstawie jego pozycji, przy jednoczesnym zachowaniu niezmienności całej tablicy.

Porównanie obu podejść

const months = ['Jan', 'Mar', 'Mar', 'Apr'];

// Old way: Spread and mutate copy
const fixedMonthsOld = [...months];
fixedMonthsOld[1] = 'Feb';

// Modern way: array.with(index, value)
const fixedMonthsNew = months.with(1, 'Feb');

console.log(months);         // ['Jan', 'Mar', 'Mar', 'Apr']
console.log(fixedMonthsNew);  // ['Jan', 'Feb', 'Mar', 'Apr']

with() obsługuje również ujemne indeksy, więc zastąpienie elementu liczonego od końca jest równie proste:

const tags = ['v1.0.0', 'v1.1.0', 'v1.2.0-beta'];
const stableTags = tags.with(-1, 'v1.2.0');

console.log(stableTags); // ['v1.0.0', 'v1.1.0', 'v1.2.0']

Skuteczne wyszukiwanie wstecz: findLast() i findLastIndex()

Używanie .find() do znajdowania elementu działa dobrze przy przeszukiwaniu od początku, ale zawsze rozpoczyna się od indeksu 0. Gdy potrzebny jest ostatni element pasujący, powszechnym rozwiązaniem było najpierw odwrócenie tablicy – co marnuje czas i niesie ryzyko zmiany danych, których nie chciano edytować.

Jaki problem to rozwiązuje

Dane takie jak wpisy dziennika, historia cofania, strumienie aktywności oraz zapisy transakcji są naturalnie uporządkowane według czasu, a często chcemy najnowszy wpis spełniający określone warunki.

Porównanie obu podejść

const logs = [
  { id: 1, action: 'LOGIN', timestamp: 1000 },
  { id: 2, action: 'UPLOAD', timestamp: 1010 },
  { id: 3, action: 'ERROR', timestamp: 1020 },
  { id: 4, action: 'UPLOAD', timestamp: 1030 },
  { id: 5, action: 'LOGOUT', timestamp: 1040 }
];

// Old way: Reversing creates overhead and array allocation
const lastUploadOld = [...logs].reverse().find(log => log.action === 'UPLOAD');

// Modern way: findLast iterates backwards directly
const lastUpload = logs.findLast(log => log.action === 'UPLOAD');
const lastUploadIdx = logs.findLastIndex(log => log.action === 'UPLOAD');

console.log(lastUpload);    // { id: 4, action: 'UPLOAD', timestamp: 1030 }
console.log(lastUploadIdx); // 3

Ponieważ skanuje on od końca i zatrzymuje się w momencie znalezienia pasującego elementu, działa w czasie O(k), gdzie k to liczba kroków wstecz do znalezienia pasującego elementu — nie ma potrzeby najpierw kopiować ani odwracać całego tablicy.

5. Łączenie Map i Filter: flatMap()

Pożądane jest łączenie metod .map() z .filter() lub .flat(), gdy każdy element źródłowy może dać zero, jeden lub kilka wyników. flatMap() łączy te operacje w jedną przejście.

Jakie problemy rozwiązuje: unika tworzenia tymczasowych tablic, które są następnie wyrzucane, gdy potrzebna jest zmiana kształtu zagnieżdżonych danych lub filtrowanie i transformacja w jednym kroku.

Przykład: spłaszczanie zagnieżdżonej listy relacji

const authors = [
  { name: 'Author A', books: ['Book 1', 'Book 2'] },
  { name: 'Author B', books: ['Book 3'] },
  { name: 'Author C', books: [] }
];

// Map + Flat produces 2 array allocations
const booksNested = authors.map(a => a.books).flat();

// flatMap runs the map and flattens depth-1 in one pass
const allBooks = authors.flatMap(a => a.books);
console.log(allBooks); // ['Book 1', 'Book 2', 'Book 3']

Przykład: jednoczesna transformacja i filtracja

Możesz odrzucić element, zwracając pustą tablicę, a zachować go, zwracając tablicę zawierającą jeden element:

const rawInputs = ['42', 'invalid', '100', 'undefined', '256'];

const validNumbers = rawInputs.flatMap(input => {
  const num = parseInt(input, 10);
  return Number.isNaN(num) ? [] : [num];
});

console.log(validNumbers); // [42, 100, 256]

6. Czystszy dostęp do elementów: at()

Dostęp do ostatniego elementu tablicy tradycyjnie oznaczał pisanie array[array.length - 1], co jest niewygodne i łatwe do błędnego zastosowania.

at() wykorzystuje konwencję indeksów ujemnych znanych z Pythona, umożliwiając liczenie wstecz od końca zarówno tablic, jak i ciągów znaków.

Przed i po

const queue = ['Alice', 'Bob', 'Charlie', 'Dana'];

// Old way
const lastPersonOld = queue[queue.length - 1];
const secondLastOld = queue[queue.length - 2];

// Modern way
const lastPerson = queue.at(-1);    // 'Dana'
const secondLast = queue.at(-2);    // 'Charlie'

Wynik jest krótszy, mniej podatny na błędy typu „off-by-one” w wyrażeniach obliczeniowych, a także lepiej współpracuje z łańcuchowymi wywołaniami metod, ponieważ nie trzeba najpierw przechowywać tablicy w zmiennej tymczasowej.

Kiedy go używać

Lista kontrolna modernizacji

  • Przeglądaj sposób zarządzania stanem: zastąp .sort() i .reverse() metodami .toSorted() oraz .toReversed(), aby uniknąć błędów wynikających z cichej mutacji.
  • Eliminuj zbędne elementy kodu: używaj Object.groupBy() zamiast ręcznie tworzonych obiektów akumulatora.
  • Zwiększ szybkość wyszukiwań: zastąp wzorce typu „odwróć, a potem znajdź” metodą findLast() w przypadku danych uporządkowanych chronologicznie.
  • Uproszcz syntaksę codziennego użytku: korzystaj z .at(-1) i .with(index, value) zamiast ręcznych obliczeń długości oraz bezpiecznych kopiowania tablic.
  • Literatura pokrewna