Tablice - metody iteracyjne

W poprzednim rozdziale dowiedzieliśmy się, że możemy iterować po tablicy korzystając z klasycznej pętli for:


const tab = ['Marcin', 'Ania', 'Agnieszka', 'Piotrek', 'Grześ', 'Magda'];

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

Można powiedzieć, że pętla for jest typowo "manualna". Trzeba ustawić licznik, kiedy się kończy i jak ma być zwiększany.

W większości przypadków nie ma nic złego w korzystaniu z tej metody, a plusem jest że metoda ta zadziała praktycznie w każdym języku. Jej główną wadą jest niestety średnia wygoda. Spójrz ile kodu musieliśmy naprodukować by zrobić zwykłą pętlę.

Poniżej przyjrzymy się kilku dodatkowym iteracyjnym metodom, które udostępniają tablice.

forEach() - automatyczna pętla po tablicy

Pierwszą z omawianych metod jest forEach(). Metoda ta przyjmuje jako parametr funkcję, w której możemy ustawić 3 parametry:

  • pierwszy parametr będzie wskazywał na dany element z tablicy,
  • drugi parametr będzie wskazywał na indeks elementu
  • trzeci będzie wskazywał na aktualną tablicę po której iterujemy

const tab = ['Marcin', 'Ania', , 'Agnieszka'];

//pod zmienną el trafią kolejne elementy
tab.forEach(function(el) {
    console.log(el.toUpperCase());
});
W dzisiejszych czasach przy wszelakich metodach iteracyjnych używa się krótszego zapisu z wykorzystaniem funkcji strzałkowej. Poniżej umyślnie zrezygnuję z tego zapisu by nie utrudniać odczytu listingów. W ramach treningu spróbuj zamienić wybrane listingi na zapis z funkcją strzałkową.

//tutaj nie wykorzystuje żadnej zmiennej więc nie muszę używać parametrów
tab.forEach(function() {
    //wykonam się tyle razy co liczba elementów
    console.log('Lubię placki');
});

//pod zmienną i będzie wstawiany indeks elementu
tab.forEach(function(el, i) {
    console.log(`Aktualny element to ${el}, a jego index to ${i}`);
});

//pod zmienną arr wstawiana będzie aktualna tablica po której iterujemy - może kiedyś to się przyda?
tab.forEach(function(el, i, arr) {
    console.log(`Aktualny element to ${el}, a długość tablicy to ${arr.length}`);
});

const tab = ['Marcin', 'Ania', 'Agnieszka'];

let letterCount = 0;
tab.forEach(function(el) {
    letterCount += el.length;
});

console.log("Liczba liter wszystkich imion to: " + letterCount); //19
A czy ostatni przykład da się zrobić łatwiej? Oczywiście. Wystarczy użyć funkcji join():

        const tab = ['Marcin', 'Ania', 'Agnieszka'];
        console.log(tab.join("").length);
    

Poza pierwszym parametrem - funkcją - metoda forEach może przyjąć jeszcze drugi parametr, który służy do ustawiania this. Domyślnie wewnątrz forEach (ale także i map, every, some, filter itp) zmienna this wskazuje na obiekt window:


const tab = ['Marcin', 'Ania', 'Agnieszka'];

tab.forEach(function() {
    console.log(this); //window
});

const ob = {
    name : "Marcin"
};

tab.forEach(function() {
    console.log(this); //ob
}, ob);

Temat ten będzie bardziej poruszany w dziale związanym z obiektami. W większości przypadków parametr ten będziemy pomijać.

every() i some()

Metody every() i some() służą do sprawdzania czy wszystkie lub czy chociaż jeden element w tablicy spełnia dany warunek.

Obie metody w rezultacie zwracają true lub false.

Metoda every() zwróci prawdę, kiedy przekazana w parametrze funkcja zwróci true dla każdego elementu w tablicy.


const tab = [1, 3, 5, 8, 9];

//sprawdzam czy wszystkie liczby są parzyste
const allEven = tab.every(function(el) {
    return el % 2 === 0;
});

console.log(allEven); //false

const tab = [
    { name : 'Marcin', age: 18 },
    { name : 'Ania', age: 16 },
    { name : 'Agnieszka', age: 16}
];

//czy wszyscy użytkownicy są pełnoletni?
const allAdult = tab.every(function(el) {
    return el.age >= 18;
});

console.log(allAdult); //false

Metoda some() zwróci prawdę, jeżeli chociaż dla jednego element użyta funkcja zwróci prawdę.


const tab = ["kot", "pies", "świnka", "jeż"];

//sprawdzam czy chociaż jedno słowo ma 3 litery
const word3letter = tab.some(function(el) {
    return el.length === 3;
});

console.log(word3letter);

const tab = [
    { name : 'Marcin', age: 18 },
    { name : 'Ania', age: 16 },
    { name : 'Agnieszka', age: 16}
];

//a może chociaż jeden user jest pełnoletni?
const isAdult = tab.some(function(el) {
    return el.age >= 18
});

console.log(isAdult); //true

map() - zwracanie nowej tablicy

Metoda map() robi pętlę po tablicy i każdorazowo zwraca nowy element tablicy. W wyniku po zakończeniu całej pętli zwracana jest nowa tablica z taką samą liczbą elementów jaką miała tablica na której ta metoda była wykonana:


const tab = ['Marcin', 'Ania', 'Agnieszka'];

const tab2 = tab.map(function(el) {
    return el.toUpperCase()
});

console.log(tab); //[Marcin, Ania, Agnieszka]
console.log(tab2); //[MARCIN, ANIA, AGNIESZKA]

const tab = [1, 2, 3];
const tab2 = tab.map(function(el) {
    return el * 2;
});

console.log(tab2); //[2, 4, 6]

const numbers = [1.2, 4.5, 9.3];

const absolute = numbers.map(function(el) {
    return Math.ceil(el);
});
console.log(absolute); //[2, 5, 10]

function multiple3(number) {
    return number * 3;
}

var ourTable = [1, 2, 3];
console.log(ourTable.map(multiple3)); //[3, 6, 9]

filter() - filtrowanie elementów

Bardzo często będziemy chcieli przefiltrować daną tablicę zwracając tylko elementy, które pasują do danego warunku.

Bardzo przydatną funkcją jest tutaj funkcja filter().
Robi pętlę po danej tablicy i w wyniku zwraca nową tablicę, zawiera odfiltrowane elementy:


const ourTable = [1, 2, 3, 4, 5, 6];

const evenNumbers = ourTable.filter(function(el) {
    return el % 2 === 0;
});

console.log(evenNumbers); //[2, 4, 6]

const ourTable = ['Marcin', 'Ania', 'Agnieszka', 'Monika', 'Piotrek'];

const woman = ourTable.filter(function(name) {
    return name.substr(-1) === 'a';
});

console.log(woman); //["Ania", "Agnieszka", "Monika"]

const tab = [
    { name : 'Marcin', age: 18 },
    { name : 'Ania', age: 16 },
    { name : 'Agnieszka', age: 16},
    { name : 'Beata', age: 22}
];

const adultUsers = tab.filter(function(user) {
    return user.age >= 18
});

console.log(adultUsers); [{ name : 'Marcin', age: 18 },  { name : 'Beata', age: 22}]

reduce() - redukowanie tablicy

Za pomocą funkcji reduce() możemy wykonywać operacje na tablicy "redukując ją", w wyniku uzyskując jakiś wynik.

Działanie tej funkcji jest następujące:
w pierwszej iteracji pod pierwszy parametr prev wstawiany jest pierwszy element tablicy, a pod next kolejny. Funkcja zwraca jakiś wynik. W kolejnej iteracji jest od podstawiany pod prev, a kolejny element w tablicy pod next. Znowu zostaje zwrócony wynik, który w kolejnej iteracji wstawiany jest w miejsce prev, a pod next znowu wstawiany jest kolejny element tablicy.


const tab = [1, 2, 3, 4];
const result = tab.reduce(function(prev, next) {
    return prev + next;
});

//1 iteracja => prev = 1, next = 2
//2 iteracja => prev = 3, next = 3
//3 iteracja => prev = 6, next = 4
//wynik = 10

const tab = [3, 2, 4, 2];
const result = tab.reduce(function(a, b) {
    return a * b;
});

//1 iteracja => prev = 3, next = 2
//2 iteracja => prev = 6, next = 4
//3 iteracja => prev = 24, next = 2

//wynik = 48

Kolejnym parametrem poza funkcją jest wartość początkowa:


//atrybut po funkcji to początkowa wartość
const sum1 = [1, 2, 3].reduce(function(a, b) {
    return a + b;
}, 0);

//sum1 = 6

const sum2 = [1, 2, 3].reduce(function(a, b) {
    return a + b;
}, "");

//sum1 = "123"

const data = [[1, 2, 3], [4, 5, 6], [7, 8, 9]];
const flatArray = data.reduce(function(total, amount) {
  return total.concat(amount);
}, []);

//flatArray = [ 1, 2, 3, 4, 5, 6, 7, 8, 9 ]

const data = [{age : 10}, {age : 12}, {age : 15}];
const age = data.reduce(function(a, b) {
  return {age : a.age + b.age};
}, {age : 0});

//{age : 37}

indexOf(), lastIndexOf() i includes() - wyszukiwanie elementu w tablicy

Wyszukać element w tablicy możemy na kilka sposobów.

Pierwszy z nich polega (podobnie jak w przypadku przeszukiwania stringów) na skorzystaniu z metody indexOf(str).

Metoda ta działa podobnie jak w przypadku stringów. Zwraca index na którym znalazła szukany tekst, lub -1, jeżeli nic nie znalazła:


const tab = ['Marcin', 'Ania', 'Agnieszka', 'Monika'];

if (tab.indexOf('Ania') !== -1) {
    console.log('Ania występuje w tablicy pod indexem ' + tab.indexOf('Ania'));
}

//lub

if (~tab.indexOf('Ania')) {
    console.log('Ania występuje w tablicy pod indexem ' + tab.indexOf('Ania'));
}

Zastosowany w drugim przykładzie znak tyldy omówiony został tutaj

Bardzo podobną metodą jest lastIndexOf(), która działa jak indexOf(), ale zwraca ostatnią pozycję szukanego tekstu:


if (tab.lastIndexOf('txt') !== -1) {
    console.log('Ostatnie wystąpienie w tablicy to', tab.lastIndexOf('txt')));
}

Podobnie do stringów dla tablic także istnieje funkcja includes(), która zwraca prawdę lub fałsz w zależności czy szukana wartość znajduje się w tablicy:


const tab = ['Marcin', 'Ania', 'Agnieszka', 'Monika'];

if (tab.includes('Ania')) {
    console.log('Ania występuje w tablicy pod indexem ' + tab.indexOf('Ania'));
}

if (!tab.includes('Pies')) {
    console.log('Pies nie występuje w tej tablicy');
}

filter() - zwracanie pasujących elementów

Metoda indexOf() i includes() sprawdzają się przy tablicy typów prostych.
A co jeżeli chcemy szukać w tablicy której dane są bardziej skomplikowane? Moglibyśmy wykonać pętlę for, ale o wiele wygodniejsze jest skorzystać z metody find(), która zwraca pierwszy pasujący element:


const tab = [
    { name : 'Karol', age: 10 },
    { name : 'Beata', age: 13 },
    { name : 'Marcin', age: 18 },
    { name : 'Ania', age: 20 },
    { name : 'Agnieszka', age: 21}
];

const findUser = tab.find(function(el) {
    return el.age >= 18
});

console.log(findUser) //{ name : 'Marcin', age: 18 }

Oczywiście find() możemy też stosować dla typów prostych:


const tab = [12, 5, 8, 130, 44];

const bigNr = tab.find(function(el) {
    return el >= 15
});

console.log(bigNr); //130

Łańcuchowość metod

Method chaining (łańcuchowość metod) to sposób odpalania kolejnych metod (funkcji), które zapisujemy po kropce. Technika ta tyczy się nie tylko tablic, ale całego JavaScript.

Każda metoda czy właściwość daje jakąś wartość. Jeżeli taka zwrócona wartość pasuje dla kolejnej metody, możemy tą metodę od razu odpalić po kropce:


const text = "Ala ma kota";

text.toUpperCase().substr(0, 3).length //kolejne metody odpalamy po kropce

//czasami trzymanie wszystkiego w jednej linii nie jest dobrym rozwiązaniem
text
    .toUpperCase()
    .substr(0, 3)
    .length

Podobnie jest z powyższymi metodami dotyczącymi tablic. Jeżeli dana metoda zwraca tablicę (np. map czy filter zwracają tablicę) możemy od razu po nich odpalać kolejne metody.


const tab = ['Marcin', 'Ania', 'Agnieszka'];


const newTab = tab.map(function(el) { //zwracam nową tablicę...
    return el.toUpperCase()
}).filter(function(el) { //więc mogę ją odfiltrować
    return el[el.length-1].toUpperCase() === "A"
}).map(function(el) { //map też może służyć do iteracji
    return el + "!";
}).forEach(function(el) {
    console.log(el);
}) //forEach nie zwraca tablicy więc nie mogę już tutaj działać jak na tablicy

console.log(newTab)

Powyższy przykład można jeszcze bardziej uprościć stosując zamiast funkcji anonimowych referencje do funkcji. Dzięki temu nie tylko nasz zapis się upraszcza, ale i zyskujemy dodatkowe "klocki" do przyszłego wykorzystania.


const tab = ['Marcin', 'Ania', 'Agnieszka'];

const upper = function(el) {
    return el.toUpperCase();
}

const checkIsWoman = function(el) {
    return el.substr(-1).toUpperCase() === "A"; //naiwny test
}

const addExclamationMark = function(el) {
    return el + "!"
}

const newTab = tab
                .map(upper)
                .filter(checkIsWoman)
                .map(addExclamationMark);

console.log(newTab)

A powyższy zapis jeszcze bardziej się uprości, jeżeli zastosujemy funkcję strzałkową


const tab = ['Marcin', 'Ania', 'Agnieszka'];

const upper = el => el.toUpperCase();
const checkIsWoman = el => el.substr(-1).toUpperCase() === "A"; //naiwny test
const addExclamationMark = el => el + "!"

const newTab = tab
                .map(upper)
                .filter(checkIsWoman)
                .map(addExclamationMark);

console.log(newTab)

findIndex() - zwracanie indeksu pasującego elementu

Metoda findIndex() zwraca indeks pierwszego pasującego elementu. Działanie tej metody jest podobne do powyższych, czyli jeżeli metoda zwróci prawdę, znaczy że dany element pasuje:


const tab = [
    { name : "Monika", gender: "w" },
    { name : "Agata", gender: "w" },
    { name : "Marcin", gender: "m" },
    { name : "Patrycja", gender: "w" }
]

const index = tab.findIndex(function(el) {
    return el.gender === "m"
})

console.log("index pasującego elementu:", index);
console.log("Pasujący element:", tab[index]);

Do czego to może się przydać? Np. do usunięcia danego elementu za pomocą splice():


const tab = [
    { name : "Monika", gender: "w" },
    { name : "Agata", gender: "w" },
    { name : "Marcin", gender: "m" },
    { name : "Patrycja", gender: "w" }
]

const index = tab.findIndex(function(el) {
    return el.gender === "m"
})

tab.splice(index, 1);

console.log(tab); //same "baby"

Trening czyni mistrza

Jeżeli chcesz sobie potrenować zdobytą wiedzę, to zadania do tego rozdziału znajdują się w w repozytorium pod adresem: https://github.com/kurs-javascript/js-podstawy w katalogu 5-tablice, przy czym śmiało możesz robić zadania z całego repozytorium.

Dowiedz się więcej na ten temat tutaj.

A może pasuje ci ściągnąć zadania do wszystkich rozdziałów na raz? Jeżeli tak, to skorzystaj z repozytorium pod adresem https://github.com/kurs-javascript/js-all.

Wszelkie prawa zastrzeżone. Jeżeli chcesz używać jakiejś części tego kursu, skontaktuj się z autorem. Aha - i ta strona korzysta z ciasteczek.