search check home clock-o tag tags chevron-left chevron-right chevron-up chevron-down twitter facebook github rss comment comments terminal code

[JS] Jak manipulować tablicami i nie tylko za pomocą JavaScript

[JS] Jak manipulować tablicami i nie tylko za pomocą JavaScript

Do napisania tego wpisu zainspirowali mnie znajomi, którzy do tej pory zajmowali się głównie PHP lub innymi językami programowania, a o JSie wiedzieli tyle, że jest i jak użyć jQuery

W tym artykule skupię się na pokazaniu jak można operować na tablicach oraz na obiektach bez potrzeby ładowania takich bibliotek jak jQuery czy lodash.

Iterowanie po tablicy za pomocą forEach

Patrząc na kod znajomych to bardzo często widziałem, że korzystali iterowania po tablicy za pomocą pętli for. Nie mówię, że to źle, ale można w takich sytuacjach wykorzystać funkcję forEach() do tego celu:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
var data = [12,'21',56,'04'];
 
///////////////
// stara wersja
for (var index = 0; index < data.length; index++) {
    console.log(data[i], i);
}
 
// wyniki:
// 12, 0
// 21, 1
// 56, 2
// 04, 3
 
//////////////
// nowa wersja
data.forEach(function (element, index) {
    console.log(element, index);
});
 
//////////
// wyniki:
//
// 12, 0
// 21, 1
// 56, 2
// 04, 3

Jak widać, zapis z wykorzystaniem funkcji forEach() jest przyjemniejszy w czytaniu a wydajność w większości przypadków jest prawie taka sama. Dodatkowo, nie trzeba się martwić o samodzielne tworzenie indeksów i manipulowanie nimi.

Parsowanie danych za pomocą funkcji map()

Bardzo często spotkałem się również z wykorzystaniem for tylko po to, aby utworzyć nową tablicę z danymi pochodzącymi z innej tablicy tylko odpowiednio przetworzonymi:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
var data = [{
    id: 24,
    name: 'Piotr',
    lastName: 'Nalepa'
}, {
    id: 25,
    name: 'Jan',
    lastName: 'Kowalski'
}, {
    id: 26,
    name: 'Anna',
    lastName: 'Nowak'
}];
var parsedData = [];
 
///////////////
// stara wersja
for (var index = 0; index < data.length; index++) {
    parsedData[index] = data[index].name + ' ' + data[index].lastName;
}
 
for (var index2 = 0; index2 < parsedData.length; index2++) {
    console.log(parsedData[index2]);
}
 
//////////
// wyniki:
//
// Piotr Nalepa
// Jan Kowalski
// Anna Nowak
 
//////////////
// nowa wersja
data.map(function (element) {
    return [element.name, ' ', element.lastName].join('');
}).forEach(function (element) {
    console.log(element);
});
 
//////////
// wyniki:
//
// Piotr Nalepa
// Jan Kowalski
// Anna Nowak

Jak widać na przykładzie powyżej, w starej wersji potrzebowaliśmy tworzyć nową tablicę aby przechować w niej przetworzone dane (po wykonaniu pierwszej pętli), a potem musieliśmy utworzyć kolejny index aby każdy element z tablicy wywołać przy pomocy funkcji console.log().

W nowej wersji to wygląda o wiele prościej, raz że za pomocą funkcji map() przetworzyliśmy dane to jeszcze mogliśmy przekazać wynik przetwarzania do następnej funkcji o nazwie forEach() i tam wywołać każdy element z przetworzoną wersją danych w funkcji console.log(). To wszystko ma jeszcze tą zaletę, że zarówno map() jak i forEach() nie modyfikują pierwotnej wersji danych ze zmiennej data tylko tworzą w pamięci nową tablicę z danymi, którą można przypisać do dowolnej zmiennej.

Filtrowanie tablicy lub wyszukiwanie elementów w tablicy za pomocą filter()

Jeśli mamy już jakieś dane w postaci tablicy, to nie raz pojawia się potrzeba aby te dane jakoś przefiltrować pod jakimś kątem i zwrócić nową tablicę.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
var data = [{
    sum: 1,
    name: 'Item §',
    id: 'i-21'
}, {
    sum: 12345,
    name: 'Item 1',
    id: 'i-32'
}, {
    sum: 99,
    name: 'Item 2',
    id: 'i-45'
}, {
    sum: 123,
    name: 'Item 3',
    id: 'i-19'
}, {
    sum: 8,
    name: 'Item 4',
    id: 'i-12'
}];
 
data.filter(function (item) {
    return item.sum > 100;
}).forEach(function (item) {
    console.log(item.name);
});
 
//////////
// wyniki:
//
// Item 1
// Item 3

Dane z tablicy znajdującej się w zmiennej data przefiltrowaliśmy pod kątem sumy większej niż 100, a następnie je wyświetliliśmy w konsoli przeglądarki.

Sumowanie danych tablicy za pomocą reduce()

Jedną z mniej znanych możliwości udostępnionych przez tablice w JS jest sumowanie wartości elementów znajdujących się w tablicy. Aby zsumować elementy należy wykorzystać funkcję reduce() w następujący sposób:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
var data = [{
    sum: 1,
    name: 'Item §',
    id: 'i-21'
}, {
    sum: 12345,
    name: 'Item 1',
    id: 'i-32'
}, {
    sum: 99,
    name: 'Item 2',
    id: 'i-45'
}, {
    sum: 123,
    name: 'Item 3',
    id: 'i-19'
}, {
    sum: 8,
    name: 'Item 4',
    id: 'i-12'
}];
 
var sum = data.reduce(function (total, next) {
    return total + next.sum;
}, 0);
 
console.log(sum);
 
//////////
// wynik:
//
// 12576

Jak widać, mając tablicę obiektów jesteśmy w łatwy sposób zsumować sobie wartości obiektów z odpowiedniej własności obiektu (w przypadku powyżej - z własności sum). Warto pamiętać o tym, że pierwszy parametr funkcji przekazywanej do wywołania to jest zawsze suma elementów już policzonych, a drugi parametr to jest następny element.

Gdybyśmy do funkcji reduce() dodali nie przekazali drugiego parametru (tj. 0) to nasze suma naszych elementów wyszłaby NaN z tego względu, że przy pierwszej iteracji dodawalibyśmy liczbę do obiektu.

Sprawdzenie czy występuje przynajmniej jeden element o określonych cechach za pomocą funkcji some()

W przypadku gdybyśmy chcieli sprawdzić czy jakikolwiek element został usunięty (wg posiadanych przez nas danych) i na tej podstawie wykonać jakąś dalszą część kodu to możemy wykorzystać funkcję some():

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
var data = [{
    isRemoved: false,
    name: 'Item §',
    id: 'i-21'
}, {
    isRemoved: false,
    name: 'Item 1',
    id: 'i-32'
}, {
    isRemoved: false,
    name: 'Item 2',
    id: 'i-45'
}, {
    isRemoved: true,
    name: 'Item 3',
    id: 'i-19'
}, {
    isRemoved: false,
    name: 'Item 4',
    id: 'i-12'
}];
 
var anyRemoved = data.some(function (item) {
    return item.isRemoved;
});
 
if (anyRemoved) {
    console.log('Jakiś element został usunięty');
}
 
//////////
// wynik:
//
// Jakiś element został usunięty

Funkcja some() zawsze zwraca albo prawdę albo fałsz. W przypadku powyżej, zwróciła prawdę i na tej podstawie wywnioskowaliśmy, że jakiś element ma status usuniętego.

Sprawdzenie czy wszystkie elementy tablicy spełniają wymagane warunki za pomocą funkcji every()

W porównaniu do funkcji some() funkcja every() sprawdza czy wszystkie elementy w tablicy spełniają wymagania:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
var data = [{
    isRemoved: false,
    name: 'Item §',
    id: 'i-21'
}, {
    isRemoved: false,
    name: 'Item 1',
    id: 'i-32'
}, {
    isRemoved: false,
    name: 'Item 2',
    id: 'i-45'
}, {
    isRemoved: true,
    name: 'Item 3',
    id: 'i-19'
}, {
    isRemoved: false,
    name: 'Item 4',
    id: 'i-12'
}];
 
var allRemoved = data.every(function (item) {
    return item.isRemoved;
});
 
if (allRemoved) {
    console.log('Wszystkie elementy mają stan usuniętego');
}
 
//////////
// wynik:
//
// [brak]

W przypadku powyżej, nie uruchomi się funkcja console.log() z tego względu, że w tablicy znajduje się tylko jeden element ze stanem usuniętego. Gdyby wszystkie elementy w tablicy miały dla własności isRemoved wartość true, to dopiero wtedy funkcja every() zwróciłaby true.

Sortowanie wyników tablicy za pomocą funkcji sort()

Elementy w tablicach można sortować na wiele różnych sposobów rosnąco bądź malejąco. W poniższym przykładzie pokażę sposób jak posortować tablicę obiektów wg ID elementu w sposób rosnący:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
var data = [{
    isRemoved: false,
    name: 'Item §',
    id: '21'
}, {
    isRemoved: false,
    name: 'Item 1',
    id: '32'
}, {
    isRemoved: false,
    name: 'Item 2',
    id: '45'
}, {
    isRemoved: true,
    name: 'Item 3',
    id: '19'
}, {
    isRemoved: false,
    name: 'Item 4',
    id: '12'
}];
 
data.sort(function (a, b) {
    return parseInt(a.id, 10) - parseInt(b.id, 10);
});
 
console.log(data);
 
//////////
// wynik:
//
// zostanie wyświetlona tablica z elementami posortowanymi wg kolejności rosnącej
// na podstawie własności `id`

Jak pewne zauważyłeś/aś, w funkcji sortującej korzystam z funkcji parseInt([liczb], 10). Jest to dobra praktyka parsowania elementy typu String to elementu typu Number przed przystąpieniem do odejmowania jednej wartości od drugiej. Gdybyśmy zamienili a z b to uzyskalibyśmy tablicę posortowaną malejąco. Co ciekawe, gdy korzystamy z funkcji sort() to nie musimy przypisywać wyniku sortowania do nowej zmiennej ponieważ pierwotna wersja tablicy (pochodząca ze zmiennej data) zostanie posortowana.

Pobranie pierwszych 10 elementów z tablicy

Czasami bywa tak, że mamy zbyt wiele danych. Potrzebujemy tylko ich część, a reszta jest zbędna i nie będzie wykorzystywana. Jeśli potrzebujemy, to możemy wykonać jakieś filtrowanie lub sortowanie. Na koniec jednak chcielibyśmy pobrać pierwsze 10 wyników z naszej tabeli z danymi. Są dwa sposoby na to. Pierwszy:

1
2
3
4
5
6
7
8
9
10
var data = [1,2,123,44,85,67,17,8,9,19,234,36,98,66,50,32];
 
data.length = 10;
 
console.log(data);
 
//////////
// wynik:
//
// zostanie wyświetlona tablica z 10 elementami

Drugi sposób jest nieco inny. Gdybyśmy jednak potrzebowali danych do późniejszego wykorzystania to możemy je zachować na później bez ich tracenia:

1
2
3
4
5
6
7
8
9
var data = [1,2,123,44,85,67,17,8,9,19,234,36,98,66,50,32];
var partialData = data.slice(0, 2);
 
console.log(partialData, data);
 
//////////
// wynik:
//
// zostanie wyświetlona tablica z 2-ma elementami oraz tablica z 16-ma elementami

Ten drugi sposób, może być stosowany w dzieleniu danych z tablicy na strony, co może się przydać w tworzeniu różnego rodzaju stronicowanych tabel lub widoków.

Usuwanie konkretnego elementu z tablicy za pomocą funkcji splice()

W przypadku gdybyśmy chcieli usunąć jakiś element z tablicy, to możemy to zrobić za pomocą poniższego kodu:

1
2
3
4
5
6
7
8
9
10
var data = [1,2,123,44];
 
data.splice(data.indexOf(123), 1);
 
console.log(data);
 
//////////
// wynik:
//
// [1,2,44]

Zastosowaliśmy tutaj dwie funkcje: indexOf() oraz splice(). Za pomocą pierwszej funkcji znaleźliśmy index elementu który chcielibyśmy usunąć, a za pomocą drugiej funkcji usunęliśmy element z tablicy.

Co ciekawe, za pomocą tej samej metody możemy zastąpić usunięty element innym: data.splice(data.indexOf(123), 1, 99);

Łączenie tablic za pomocą funkcji concat()

W przypadku, gdy mamy dwie tablice i chcielibyśmy je złączyć w jedną tablicę to wystarczy użyć funkcji concat():

1
2
3
4
5
6
7
8
9
10
11
12
var data1 = ['1', 2];
var data2 = ['a', 'b'];
var total;
 
total = data1.concat(data2);
 
console.log(total);
 
//////////
// wynik:
//
// ['1', 2, 'a', 'b']

Za pomocą funkcji concat() możemy łączyć wiele tablic w jedną. Wystarczy podać kolejne tablice jako kolejne parametry we funkcji concat().

Odwracanie wartości w tablicy za pomocą funkcji reverse()

Czasami trzeba odwrócić wartości w tablicy. Jest do tego osobna funkcja:

1
2
3
4
5
6
7
8
9
10
var data = [2,5,9,3];
 
data.reverse();
 
console.log(data);
 
//////////
// wynik:
//
// [3, 9, 5, 2]

Osobiście, niezbyt często korzystam z tej funkcji, ale czasem bywa przydatna.

Iterowanie po obiektach (lub innymi słowy, po tablicy asocjacyjnej)

W świecie PHP bardzo ma się do czynienia z tablicami asocjacyjnymi, które w świecie JavaScript są obiektami. Nie ma oczywistego sposobu na iterowanie po kluczach w obiekcie, tak by brał pod uwagę tylko własne klucze, a nie również dziedziczone (tego tematu nie będę omawiał tutaj). Sposobem z którego najczęściej korzystam to:

1
2
3
4
5
6
7
8
9
10
11
12
var data = {
    name: 'Piotr',
    id: 21,
    details: {
        job: 'developer',
        city: 'Katowice'
    }
};
 
Object.keys(data).forEach(function (key) {
    console.log(data[key]);
});

Najpierw listuję wszystkie klucze obiektu do tablicy, a następnie iteruję po nich i w ten sposób nie muszę znać kluczy, aby dobrać się do wartości ukrytych pod kluczami.

Podsumowanie

Tym artykułem chciałem wskazać kilka sposobów na operowanie na tablicach osobom, które nie są na bieżąco z informacjami o JavaScripcie lub przychodzą z innych języków programowania, a w ich głowach panuje przesąd, że JavaScript niewiele się zmienił od 2007 roku 😉

Mam nadzieję, że ta dawka wiedzy pozwoli Ci na sprawne manipulowanie danymi w swoim kodzie i nie będzie trzeba korzystać cały czas z pętli for do wszystkiego.

  • Nie wspomniałeś o tym, że tablice w JS są obiektami iterowalnymi od ES6 😉

  • Maciej Cąderek

    Spoko artykuł, ale pisanie, że reduce służy do sumowania elementów tablicy to karygodne uproszczenie – to najogólniejsza funkcja z tu zaprezentowanych, umożliwia wykonanie wszystkich operacji przeprowadzanych przez pozostałe funkcje wyższego rzędu.

  • Bardzo dobry artykuł, zwłaszcza dla osób, które na co dzień pracują w innych językach.
    Na pewno będę tutaj zaglądał częściej 🙂 Dobra robota.

  • Masz rację, ale chciałem się skupić na prostych przykładach, które uznałem za najczęściej występujące.
    Dziękuję za komentarz 🙂

  • Celem tego wpisu było pokazanie podstaw. Mówienie o ES6 osobom, które nie rozróżniają nowych standardów od starszych nie miałoby celu.

  • Interesujące zestawienie. Gratuluję.

    Co mnie zdziwiło to zdanie dotyczące tej samej wydajności pętli `for` oraz `forEach`.
    Nie jestem do końca przekonany, czy faktycznie wydajności obu są zbliżone do siebie.

    Po drugie to ciężko zgodzić mi się z użyciem `parseInt` jako dobrej praktyki do rzutowania stringa na liczbę.
    Do tego mamy konstruktor `Number`. Prosty przykład:
    „`
    let a = ‚1.2’;
    console.log(parseInt(a, 10)); // 1
    console.log(Number(a)); // 1.2
    „`
    @piotr_nalepa:disqus też kiedyś używałem `parseInt` do wykonania procesu konwersji na number, jednak nauczony doświadczeniem, zostawiam realizację tego zadania konstruktorowi `Number`.

  • Zacznijmy może od końca: to zależy, jaką liczbę chcesz 😉 Twierdzenie, że parseInt nie jest dobrą praktyką, bo nie produkuje floata, jest IMO nieporozumieniem. Używam parseInt właśnie dlatego, że NIE chcę floata. Gdybym zamiast liczby całkowitej chciał mieć liczbę z przecinkiem, użyję parseFloat. Z kolei gdy chcę jakąkolwiek liczbę – Number pasuje idealnie.

    Co do for i forEach: fakt, for jest o wiele szybsze niemniej jego używanie na dłuższą metę po prostu boli:

    „`
    var arr = [ 1, 2, 3, 4 ],
    i;

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

    // vs

    arr.forEach( function( val ) {
    console.log( val );
    } );
    „`

    IMO w 98% przypadków lepiej użyć czegoś, co wygląda sensowniej (i ładniej) niż skupiać się na szybkości. Inna rzecz, że w dobie ES6 warto się zastanowić, czy forEach nie boli w porównaniu z for…of 😉

  • @Comandeer:disqus chyba nie zrozumiałeś mojego wpisu 🙂

    Słowa autora posta:
    „Jak pewne zauważyłeś/aś, w funkcji sortującej korzystam z funkcji `parseInt([liczba], 10)`. Jest to dobra praktyka parsowania elementy typu String to elementu typu Number przed przystąpieniem do odejmowania jednej wartości od drugiej.” – Nie zgadzam się. To konstruktor Number jest to rzutowania stringa na number. Funkcja globalna `parseInt` konwertuje coś na integera (stara się), a `parseFloat` to liczby zmiennoprzecinkowej.

    Rozumiem Twoje podejście. Rozumiem, że używasz parseInt z głową. Super. Tak powinno być. Jednak w takim przykładzie lepsze jest użycie odpowiedniego konstruktora.

    Słowa autora posta:
    „Jak widać, zapis z wykorzystaniem funkcji forEach() jest przyjemniejszy w czytaniu a wydajność w większości przypadków jest prawie taka sama.” – Nie zgadzam się. Rozumiem zalety forEach. Jasne, że kod jest przyjemniejszy. Ale wydajność nie jest taka sama.

    No offence 😊

  • > chyba nie zrozumiałeś mojego wpisu

    Chyba jednak tak, bo napisałeś praktycznie to samo, co ja 😛

    > Jednak w takim przykładzie lepsze jest użycie odpowiedniego konstruktora.

    Zgadzam się.

    > Jasne, że kod jest przyjemniejszy. Ale wydajność nie jest taka sama.

    I to napisałem 😉

    > Co do for i forEach: fakt, for jest o wiele szybsze niemniej jego używanie na dłuższą metę po prostu bol

  • Od siebie dodam jeszcze tylko jeden przykład:

    let arr1 = […document.getElementsByClassName(‚klasa’)],
    arrr2 = […document.querySelectorAll(‚selector’)];

    I voila, mamy referencje DOM zapisane w tablicach dziedziczących po Array.prototype, i możemy na nich stosować wszystkie omawiane przez Ciebie metody, w tym np. filter w celu odpowiedniego wyodrębnienia określonych elementów… i jQuery przestaje być potrzebne w prostych zadaniach webmasterskich…

  • Dziękuję za wartościowy komentarz. Celowo nie poruszałem wówczas tematu związanego z ES6.