Iteracja po tablicy w JavaScript

Cześć! To jest fragment książki JavaScript od podstaw, która ma pomóc w nauce programowania od zera.

Kiedy mamy jakiś zbiór elementów, często potrzebujemy zrobić coś dla każdego z nich, na przykład wyświetlić je kolejno albo sprawdzić, czy wszystkie spełniają określone warunki.

W programowaniu, przejście przez wszystkie elementy zbioru określa się jako iteracja lub enumeracja1. Najprostszym przykładem iteracji może być wypisywanie do konsoli kolejnych elementów tablicy. W JavaScript możemy to zrobić przy użyciu poznanej już pętli for.

const shoppingList = ["Jabłko", "Banan", "Śliwka"];

for (let i = 0; i < shoppingList.length; i++) {
  const item = shoppingList[i];
  console.log(item);
}

// Wypisze:
// Jabłko
// Banan
// Śliwka

Dokładniej to zjawisko w praktyce zobrazuje przykład, gdzie listę zakupów zamieniamy na pojedynczy tekst, z elementami oddzielonymi przecinkami. Taki tekst łatwiej byłoby chociażby wyświetlić na stronie.

const shoppingList = ["Jabłko", "Banan", "Śliwka"];

let shoppingListText = "";
for (let i = 0; i < shoppingList.length; i++) {
  const item = shoppingList[i];
  shoppingListText += item + ", ";
}

console.log(shoppingListText); // Jabłko, Banan, Śliwka,

Warto zauważyć pewną niedoskonałość w powyższym kodzie — tekst kończy się niepotrzebnym przecinkiem. Moglibyśmy się go pozbyć poprzez dodanie warunku, który stawia przecinek tylko i wyłącznie, jeśli nie mamy do czynienia z ostatnim elementem.

const shoppingList = ["Jabłko", "Banan", "Śliwka"];

let shoppingListText = "";
for (let i = 0; i < shoppingList.length; i++) {
  const item = shoppingList[i];
  shoppingListText += item;
  if (i !== shoppingList.length - 1) {
    shoppingListText += ", ";
  }
}

console.log(shoppingListText); // Jabłko, Banan, Śliwka

Taka implementacja2 jest dość złożonym, aczkolwiek często powtarzanym wzorcem. Dlatego powstały dla niego nowocześniejsze alternatywy, które poznamy już za chwilę. Aby jednak do nich dojść, zacznijmy od funkcji forEach.

Funkcja forEach

Iteracja po elementach tablicy to nagminnie powtarzany wzorzec. Nie powinno więc dziwić, że programiści woleli stworzyć funkcję, która się tym zajmuje. Taką funkcję zwyczajowo nazywa się forEach. Zastanówmy się, jak moglibyśmy ją sami napisać? Powinna ona zawierać części uniwersalne, takie jak:

  • pętlę for,
  • definiowanie i zmianę wartości zmiennej reprezentującej indeks aktualnego elementu oraz
  • odnoszenie się do tego elementu przy jej pomocy.
// Stałe elementy iteracji po tablicy
for (let i = 0; i < array.length; i++) {
  const element = array[i];
  // Operacja na elemencie
}

Powinna także przyjmować jako argumenty to, co jest zmienne:

  • tablicę, po której iterujemy,
  • operację do wykonania na każdym elemencie.

Przekazanie operacji nie jest dla nas zupełnie nowym pojęciem, gdyż poznaliśmy go już na końcu rozdziału o funkcjach. Czy pamiętasz callFunction? Chodzi o przekazanie funkcji jako argumentu. Tutaj musimy zrobić to samo, z tą różnicą, że funkcję wywołamy nie raz, a tyle razy, ile jest elementów. Dla każdego elementu wywołamy tę funkcję, a element przekażemy jako argument.

function forEach(array, operation) {
  for (let i = 0; i < array.length; i++) {
    const element = array[i];
    operation(element);
  }
}

const shoppingList = ["Jabłko", "Banan", "Śliwka"];
forEach(shoppingList, function(item) {
  console.log(item);
});
// Wypisze:
// Jabłko
// Banan
// Śliwka

let shoppingListText = "";
forEach(shoppingList, function(item) {
  shoppingListText += item + ", ";
});
console.log(shoppingListText); // Jabłko, Banan, Śliwka,

Aby lepiej zrozumieć działanie forEach, przeanalizujmy jedno z wywołań, używając objaśnienia funkcji poprzez podstawienie jej ciała. Zrobimy to dwustopniowo: najpierw podstawimy wywołanie funkcji forEach, a następnie operation.

const shoppingList = ["Jabłko", "Banan", "Śliwka"];
forEach(shoppingList, function(item) {
  console.log(item);
});

// Jest jednoznaczne z:
const shoppingList = ["Jabłko", "Banan", "Śliwka"];
const operation = function(item) {
  console.log(item);
}
for (let i = 0; i < shoppingList.length; i++) {
  const element = shoppingList[i];
  operation(element);
}

// Jest jednoznaczne z:
const shoppingList = ["Jabłko", "Banan", "Śliwka"];
for (let i = 0; i < shoppingList.length; i++) {
  const element = shoppingList[i];
  console.log(element);
}

Powyższy przykład wyjaśnia działanie funkcji forEach w jej aktualnej formie. Nie jest ona jednak skończona. Co zrobić w sytuacji, gdy przy iteracji po tablicy potrzebujemy nie tylko samego elementu, ale także jego indeksu? Na przykład: jak pozbyć się ostatniego przecinka w naszym wyliczeniu?

W rozdziale o funkcjach pisaliśmy, że możemy przekazać do funkcji więcej argumentów niż ma ona parametrów. Te nadprogramowe elementy zostaną przez nią po prostu zignorowane.

const print = function(element) {
  console.log(element);
}

print(); // undefined
print("Jabłko"); // Jabłko
print("Banan", 1); // Banan

Korzystając z tej właściwości, możemy napisać funkcję forEach, która będzie przekazywała indeks na drugiej pozycji. Nic nie stoi na przeszkodzie, aby wykorzystywać ją tak jak poprzednio, gdyż nadmiarowy argument w tym przypadku zostanie zignorowany. Dodatkowo będzie można też wtedy umieścić drugi parametr oznaczający indeks danego elementu.

function forEach(array, operation) {
  for (let i = 0; i < array.length; i++) {
    const element = array[i];
    operation(element, i);
  }
}

const shoppingList = ["Jabłko", "Banan", "Śliwka"];

let shoppingListText = "";
forEach(shoppingList, function(item, index) {
  shoppingListText += item;
  if (index !== shoppingList.length - 1) {
    shoppingListText += ", ";
  }
});

console.log(shoppingListText); // Jabłko, Banan, Śliwka

Z uwagi na wygodę i częstotliwość występowania funkcji forEach została ona dodana w JavaScript jako metoda tablicy3. Nie musimy jej więc sami definiować, aby móc z niej skorzystać.

const shoppingList = ["Jabłko", "Banan", "Śliwka"];

shoppingList.forEach(function(item) {
  console.log(item);
});

// Wypisze:
// Jabłko
// Banan
// Śliwka

let shoppingListText = "";
shoppingList.forEach(function(item) {
  shoppingListText += item + ", ";
});

console.log(shoppingListText); // Jabłko, Banan, Śliwka,

let shoppingListBetterText = "";
shoppingList.forEach(function(item, index) {
  shoppingListBetterText += item;
  if (index !== shoppingList.length - 1) {
    shoppingListBetterText += ", ";
  }
});

console.log(shoppingListBetterText);
// Jabłko, Banan, Śliwka

Ćwiczenie: forEach

Używając metody forEach napisz funkcje:

  • printAllValues, która dla każdej wartości w przekazanej przez argument tablicy opisze jej wartość zgodnie z szablonem 'Na pozycji {indeks} znajduje się "{wartość}"',
  • sumAll, która zwróci sumę liczb w tablicy przekazanej jako argument.

Przykłady użycia poniżej.

printAllValues([true]);
// Na pozycji 0 znajduje się "true"
printAllValues(["A", 12, undefined]);
// Na pozycji 0 znajduje się "A"
// Na pozycji 1 znajduje się "12"
// Na pozycji 2 znajduje się "undefined"

console.log(sumAll([])); // 0
console.log(sumAll([1, 2])); // 3
console.log(sumAll([3, 4, 5])); // 12

Odpowiedzi na końcu książki.

Funkcja map

Kolejną częstą operacją jest zmiana każdego elementu tablicy w określony sposób. W takim przypadku mówimy o mapowaniu elementów. Dla przykładu: naszą listę zakupów moglibyśmy przemapować tak, by produkty były pisane wielkimi literami (przy użyciu wbudowanej metody toUpperCase), albo moglibyśmy przemapować liczby na ich kwadraty.

const shoppingList = ["Jabłko", "Banan", "Śliwka"];
const numbers = [1, 2, 3, 4];

const upper = [];
shoppingList.forEach(function (item) {
  upper.push(item.toUpperCase());
});
console.log(upper); // [ 'JABŁKO', 'BANAN', 'ŚLIWKA' ]

const numberSquares = [];
numbers.forEach(function (item) {
  numberSquares.push(item * item);
});
console.log(numberSquares); // [ 1, 4, 9, 16 ]

Funkcja, która mogłaby się zająć takim mapowaniem, musiałaby przyjmować:

  • tę samą tablicę,
  • dokładną transformację, jakiej chcemy dokonać na elementach.

Zwróciłaby natomiast nową tablicę powstałą w wyniku transformacji wszystkich elementów. Tradycyjnie nazwiemy tę funkcję map. Do jej napisania możemy wykorzystać poznaną już metodę forEach. Utworzymy też tablicę, w której umieścimy przemapowane elementy.

function map(array, transformation) {
  const newArray = [];
  array.forEach(function (item) {
    newArray.push(transformation(item));
  });
  return newArray;
}

const shoppingList = ["Jabłko", "Banan", "Śliwka"];
const numbers = [1, 2, 3, 4];

const upper = map(shoppingList, function(item) {
  return item.toUpperCase();
});
console.log(upper); // [ 'JABŁKO', 'BANAN', 'ŚLIWKA' ]

const numberSquares = map(numbers, function(item) {
  return item * item;
});
console.log(numberSquares); // [ 1, 4, 9, 16 ]

Żeby lepiej zrozumieć jak ona działa, przeanalizujmy ją przez podstawienie.

const upper = map(shoppingList, function(item) {
  return item.toUpperCase();
});

// Jest jednoznaczne z:
const array = shoppingList
const transformation = function(item) {
  return item.toUpperCase();
};
const newArray = [];
array.forEach(function (item) {
  newArray.push(transformation(item));
});
const upper = newArray

// Jest jednoznaczne z:
const upper = [];
shoppingList.forEach(function (item) {
  upper.push(item.toUpperCase());
});

Funkcja map — podobnie jak forEach, została zdefiniowana w JavaScript jako wbudowana metoda tablicy.

const shoppingList = ["Jabłko", "Banan", "Śliwka"];
const numbers = [1, 2, 3, 4];

const upper = shoppingList.map(function(item) {
  return item.toUpperCase();
});
console.log(upper); // [ 'JABŁKO', 'BANAN', 'ŚLIWKA' ]

const numberSquares = numbers.map(function(item) {
  return item * item;
});
console.log(numberSquares); // [ 1, 4, 9, 16 ]

Jednym z popularnych przykładów użycia może być zamiana zbioru obiektów na listę elementów widoku. Tak więc poniżej widzimy listę kursów — wykorzystajmy tę metodę, by zamienić ją na listę elementów widoku, które możemy wyświetlić.

Przykład użycia map w aplikacji Kt. Academy. Została napisana z wykorzystaniem React. Zrzut ekranu wykonany w WebStorm.

Ćwiczenie: map

Napisz funkcję toFullNames, która zamienia listę użytkowników na listę ich pełnych imion i nazwisk w formacie "{imię} {drugie imię} {nazwisko}", lub "{imię} {nazwisko}" jeśli nie ma drugiego imienia. Załóż, że użytkownicy mają właściwości firstName, lastName o typie string, oraz secondName typu string lub undefined.

Przykłady użycia poniżej.

const users = [
  {firstName: "Turanga", lastName: "Leela"},
  {firstName: "Amy", lastName: "Wong"},
  {
    firstName: "Philip",
    secondName: "Jay",
    lastName: "Fry"
  },
  {
    firstName: "Bender",
    secondName: "Bending",
    lastName: "Rodríguez"
  },
];

const fullNames = toFullNames(users);
fullNames.forEach(function (name) {
  console.log(name);
});
// Turanga Leela
// Amy Wong
// Philip Jay Fry
// Bender Bending Rodríguez

Odpowiedzi na końcu książki.

Funkcja join

Obiecałem Ci jeszcze lepszy sposób na łączenie kilku tekstów w jeden — jest nim metoda join. Zamienia ona tablicę na pojedynczy tekst. W argumencie określamy, jak powinny być oddzielone wartości.

var fruits = ["Jabłko", "Gruszka", "Pomarańcz"];
console.log(fruits.join(", "));
// Jabłko, Gruszka, Pomarańcz
console.log(fruits.join(" + "));
// Jabłko + Gruszka + Pomarańcz

Jeśli metoda join otrzyma obiekty innego typu niż stringi, to sama zamieni je na teksty, niekoniecznie tak jak byśmy chcieli. Dlatego często przed nią stoi map zamieniający obiekty na stringi w pożądany sposób.

users
  .map(function(user) {
    return user.name;
  })
  .join(", "); // Marta, Kasia, Asia

Inne metody tablicy warte poznania

Powstało wiele metod tablicy, które warto poznać. Szczególnej uwadze polecam:

  • filter — do określania, które elementy powinny zostać,
  • find — do znalezienia elementu spełniającego określone kryteria,
  • sort — do sortowania zgodnie z założonym porządkiem,
  • reduce — do akumulowania wartości tablicy do innej wartości lub zbioru (przy jej pomocy łatwo można napisać między innymi filter czy map).

Zapoznanie się z nimi potraktuj jako pracę domową (dla chętnych). By jednak nie komplikować tej książki, poprzestanę na opisanych w tym rozdziale funkcjach.

Użyteczność metod tablicy

Metody takie jak forEach czy map zyskały we współczesnych projektach taką popularność, że niemalże zupełnie wyparły klasyczne pętle, takie jak for czy while. Kiedyś były one uważane za zaawansowane funkcjonalności, tymczasem dziś traktuje się je jako standard. Zwłaszcza gdy wykorzystujemy je w połączeniu z funkcjami strzałkowymi, którym poświęciłem następny rozdział.

1:

Iteracja od łacińskiego iteratio, czyli powtarzanie. Enumeracja od łacińskiego enumeratio, czyli wyliczenie, wyszczególnienie. Obydwa pojęcia są bardziej ogólne. Formalnie iteracja to czynność powtarzania tej samej operacji w pętli, z góry określoną ilość razy lub aż do spełnienia określonego warunku. W zasadzie więc pętle while i for wykonują iterację. Enumeracją zaś zwykle nazywa się iterację po kolekcji lub po zbiorze zasobów. Pojęcie enumeracji konkretniej odnosi się zatem do tablic. Pojęcia te są jednak często używane zamiennie. W środowiskach JavaScript bardziej powszechnym pojęciem wydaje mi się być iteracja, więc to jej postanowiłem się trzymać.

2:

Implementacja jako czasownik to proces pisania programu (kodu źródłowego), a jako rzeczownik rozumiana jest jako efekt tego procesu, czyli ukończony kod. Jest to bardzo popularne pojęcie w programowaniu. Pojęcie znajduje się także w słowniczku na końcu książki.

3:

Wbudowana funkcja forEach pozwala na jeszcze jeden parametr w przekazywanej funkcji. Pierwszym jest element, drugim jego indeks, a trzecim cała tablica.