SłownikiNr.3 Czy wiesz, że tablica nie jest jedyną kolekcją w JavaScript. Poza tym sama tablica oferuje dużo metod pomocniczych i nie musisz pisać swoich.

Dzisiaj spojrzymy na to wszystko. W końcu to trening JavaScript.

Wraz specyfikacja EcmaScript 6 i dalej pojawiło się mnóstwo dodatkowych funkcji dla tablic. Zacznijmy od problemu, który możesz przypadkiem popełnić. Tworzymy niby tablice i umieszczamy w niej wartość 200.000. Programista może założyć, że jest to wartość, która idzie do tablicy. Co jednak pojawi się w konsoli.

let bills = Array(200000);
console.log(bills.length);

Otóż właśnie utworzyłeś tablice, która ma 200.000 elementów undefined. Może wydawać się to nie intuicyjne, ale takie błędy się zdarzają.

Do EcmaScript 6 doszła funkcja statyczną "Array.of". Jak myślisz, co się stanie, gdy to wykonanym. 

let bills = Array.of(200000);
console.log(bills.length);

Teraz do tablicy trafi tylko jeden element o wartości: 200.000.

Co robić, gdy chcesz zmienić zawartość tablicy jakąś stałą funkcją. Powiedzmy, że chciał listę swoich rachunków zwiększyć o 20%. Czy potrzebuje do tego pętli for? Oczywiście, że nie

Korzystają z funkcji statycznej "from" i funkcji strzałkowej mogę stworzyć nową tablicę z rachunkami większymi o 20%.

let bills = [500,250,50,60];

let newbills = Array.from(bills, v => v*1.2);
console.log(newbills);

W konsoli dostanę nowe wartości swoich rachunków.

Teraz skorzystam z tej samej metody, tylko użyje z "normalnej" funkcji. Dodam też trzeci parametr i jest to obiekt z właściwością "addmore". Co pojawi się w konsoli?

let bills = [500,250,50,60];

let newbills2 = Array.from(bills,function(v) {

    return v + this.addmore;
}, {addmore:30});
console.log(newbills2);

!_02.png

Dostanę tablicę z nowymi wartościami zwiększonymi o 30. Trzeci parametr reprezentuje więc obiekt, który będzie w słowie kluczowym THIS w tej funkcji.

Teraz czy mogę zrobić to samo z funkcją strzałkową.

let bills = [500,250,50,60];

let newbills2 = Array.from(bills,v => v+this.addmore, 
{addmore:30});
console.log(newbills2);

Dostaniesz tablicę z wartościami NaN. Te wartości powstały w wyniku dodawania wartości liczbowej z typem undefined. Pamiętaj, że w funkcji strzałkowej nie pozwalają Ci zmienić słowo kluczowe THIS. Słowo kluczowe THIS w funkcji strzałkowej będzie zarezerwowany dla kontekstu, w którym on się pojawia.

Spójrzmy na funkcję instancji tablic, która nazywa się fill(). Nazwa sugeruje, że wypełni ona obiekty czymś. Co pojawi się w konsoli.

let bills = [500,250,50,60];

bills.fill(100);
console.log(bills);

Dostaniesz tablicę wypełnioną wartościami 100.

[100, 100, 100, 100]

Oto taki sam przykład tylko do metody przekazujemy drugi parametr i jest to dwa. Jak myślisz co to, znaczy? Co pojawi się w konsoli?

let bills = [500,250,50,60];

bills.fill(100,2);
console.log(bills);

Dostaniesz następującą tablicę

[500, 250, 100, 100]

Drugi parametr więc określa, od którego elementu indeksu ma wypełniać tablicę tą wartością. Index 2 to 3 element, gdyż indeks tablic zaczyna się od 0.

Wiesz, że ta funkcja ma 3 parametr. Co teraz?

let bills = [500,250,50,60];

bills.fill(100,2,3);
console.log(bills);

Dostaniesz tablicę:

[500,250,100,60]

Trzeci parametr reprezentuje indeks, kiedy to wypełnianie ma się skończyć. Wypełniamy w tym przypadku tylko element na indeksie 2.

A wiesz, że możesz przekazać liczbę ujemną do pierwszego parametru. Co się wtedy stanie?

let bills = [500,250,50,60];

bills.fill(100,-3);
console.log(bills);

Dostaniesz tablice:

[500, 100, 100, 100]

Czyli wypełniamy od końca 3 element w tablicy i tak dalej. Minus więc określa liczenie od końca tablicy. 

Instancje tablicy mają też funkcję find(). Używając jej, spróbuje znaleźć rachunek większy niż 450. Jak myślisz, co pojawi się konsoli.

let bills = [500,250,50,60];

let result = bills.find(value => value >=250);
console.log(result);

!_03.png

Dostanę oczywiście wartość 500. Warto zaznaczyć, że ta funkcja znajduje pierwszy element, który spełnia warunek. Warto o tym pamiętać, gdyż być może się spodziewałeś, że otrzymasz tablice wartości zgodny z tym kryterium. 

Istnieje podobna funkcja "findIndex", która zwróci indeks zamiast wartości. W funkcji może operować na podstawie wielu rzeczy jak: index, wartość, jak i samą tablicą. W tym przypadku chce zwrócić indeks, gdy wartość równa się THIS, a w tym przypadku THIS jest wartością 50.

Co pojawi się w konsoli?

let bills = [500,250,50,60];

let result = bills.findIndex(function (value, index, array)
{
    return value == this;
}, 50);

console.log(result);

!_04.png

W konsoli otrzymam indeks 2.

Pora na kolejną funkcję "copyWithin".  Jak nazwa wskazuje:  kopiuje ona dane wewnątrz tablicy. Co się stanie, gdy przekaże do niej 2 i 0.

let bills = [500,250,50,60];

bills.copyWithin(2,0);
console.log(bills);

Dostaniesz tablice:

[500, 250, 500, 250]

Czyli od 2 indeksu, czyli 3 elementu wykonało się kopiowanie od pierwszego elementu tablicy. Pierwszy element miał wartość 500.

Może wydawać się, to nie jasne więc stwórzmy prostszą tablicę z wartościami od 0 do 7. Teraz przekaże parametr 3 i 0 do funkcji. Co się stanie.

let numbers = [0,1,2,3,4,5,6,7];
numbers.copyWithin(3,0);
console.log(numbers);

Otrzymam tablicę:

[0, 1, 2, 0, 1, 2, 3, 4]

Jak widzisz, od 4 elementu wykonało się kopiowanie.

Teraz co się stanie, gdy podam 3 parametr do tej funkcji.

let numbers = [0,1,2,3,4,5,6,7];
numbers.copyWithin(3,0,2);
console.log(numbers);

Otrzymam tablicę:

[0, 1, 2, 0, 1, 5, 6, 7]

Jak w poprzedniej funkcji tablicowej 3 parametr określa, kiedy kopiowanie ma się skończyć. Jak widzisz, czwarty element ma wartość 0, a piąty element ma wartość 1. Później operacja się kończy i dalsze wartości tablicy są po staremu.

Pora pokazać ci funkcje entries() dodatkowo użyje na nim operatora spread. Bez niego dostaniesz symbol iteratora tablicy. Tak w JavaScript możesz dostać obiekt symbolizujący iterację. Co otrzymam w konsoli.

let names = ['Mortal Kombat','Dune 2','Settlers'];
console.log(...names.entries());

//to samo
for (let e of names.entries()) {
  console.log(e);
}

Dostaniesz 3 nowe obiekty, które są tablicą mającą tylko dwie wartości. Pierwszy element tej tablicy reprezentuje indeks, a drugi element wartość.

[0, "Mortal Kombat"], [1, "Dune 2"], [2, "Settlers"]

Możesz też zobaczyć co funkcja "keys()" zwróci dla tablic.

let names = ['Mortal Kombat','Dune 2','Settlers'];
console.log(...names.keys());

Dostaniesz tablicę indeksów. W sumie to są klucze w tablicy. Analogicznie możesz wyświetlić wartości tablicy w taki sposób.

let names = ['Mortal Kombat','Dune 2','Settlers'];
console.log(...names.values());

Warto też wspomnieć o trochę nowszych metodach tablicy, o których być może nie słyszałeś.

Funkcja includes sprawdza, czy dane element jest tablicy. Jeśli tak jest, to dostaniesz prawdę.

var basketOfFoods = ['🍎', '🍏', '🍇', '🍌', '🥕'];
if (basketOfFoods.includes('🍌')) {
  console.log('You have a banna');
}

Drugi parametr w tej funkcji określa czy dany element występuje od pewnego indeksu w tablicy.

var buildingInFire = [' ', ' ',' ', '🔥', '🔥', '🔥', ' '];
if (buildingInFire.includes('🔥', 4)) {
  console.log('Mam pożar na 4 piętrze i wyżej');
}

W JavaScript często możesz mieć tablice zawierająca kolejną tablicę. Masz jednym słowem bałagan i chciałbyś to wszystko spłaszczyć. Nie ma problemu, od czego funkcja flat() z parametrem stopnia spłaszczenia.

var basketOfFoods = [['🍎','🍎'], '🍏', ['🍇','🔥'], ['🍌'], ['🥕']];
var newarray = basketOfFoods.flat(1);
console.log(newarray);

Na koniec warto wspomnieć o funkcji, z której będziesz najczęściej korzystał, czyli o funkcji, która przekształca jedną tablicę elementów w drugą. Oto funkcja map.

var numbers = [1, 5, 10, 15];
var twotimes = numbers.map(function(x) {
    return ;
});
var treetimes = numbers.map(x => x * 3);
var square = numbers.map(Math.sqrt);

var wonderfull = numbers.map(function(x) {
    return ({twotimes : x * 2,threetime: x * 3,
                square :Math.sqrt(x) })
});

console.log(twotimes);
console.log(treetimes );
console.log(square);
console.log(wonderfull);

To wszystko na temat funkcji pomocnicy w tablicach pora zobaczyć inną kolekcję w JavaScript.

Map i WeakMap

Od EcmaScript 6 mam nowe klasy, które mogą nam ułatwić pracę nad kolekcjami. Jedną z nich jest Map i WeakMap.

W sumie to używamy Map cały czas w JavaScript. Każdy obiekt mógłby być postrzegany za mapę. Obiekt w końcu składa się z właściwości i wartości. Mógłbyś pomyśleć o nich jak o kluczach i wartościach.

var obj = new Object();
obj.a = 1;
obj.b = 2;
obj[0] = 3;
console.log(obj['a']);
console.log(obj['b']);
console.log(obj[0]);

!_05.png

Konsola pokaże Ci: 1,2,3

Spójrz na bardziej zaawansowany przykład. Teraz traktuje cały obiekt jako klucz.

var obj = new Object();
var objKey = ({id:1});
obj[objKey] = 4;
console.log(obj[objKey]);
console.log(Object.getOwnPropertyNames(obj));

W konsoli otrzymasz: 4, ["[object Object]"]

Na razie wydaje się, że wszystko jest w porządku. Chociaż indeks  ["[object Object]"] wydaje się podejrzany.

Pora na kolejny przykład.

var obj = new Object();
var objKey1 = ({id:1});
var objKey2 = ({id:2});
var objKey3 = ({id:3});
obj[objKey1] = 4;
obj[objKey2] = 5;
obj[objKey3] = 6;
console.log(obj[objKey1]);
console.log(Object.getOwnPropertyNames(obj));

!_06.png

W konsoli otrzymasz: 6, ["[object Object]"]

Jak widzisz klasyczny obiekt JavaScript, nie może być traktowany jak słownik.

W obiekcie wszystkie klucze  muszą być napisem lub liczbą. Nie możesz użyć innego obiektu jako klucza. 

To jest właśnie powód, dla którego mamy klasy Map i WeakMap. Czym dokładnie jest jednak ta WeakMap. Co powoduje, że jest ona taka słaba.

Powiedzmy, że istnieje obiekt jako klucz w WeakMap. Później wszystkie referencje do tego obiektu w JavaScript znikają w któryś momencie. Silnik JavaScript usunie tę referencję z WeakMap.

WeakMap więc nie trzyma się tego obiektu mocno. Gdy obiekty mają być posprzątane, to zostaną one automatycznie usunięte z WeakMap. Klucze w WeakMap nie są więc stałe. Jest to jeden z powodów, dlaczego nie można zrobić iteracji po kluczach w WeakMap, jak i pokazać wszystkie wartości przechowane w WeakMap.

Zobaczmy przykłady.

Oto przykład zwykłej mapy. Tworze dwa klucze, które są obiektami i ustawiam im ich wartości.

Później przy pomocy klucza chce pobrać konkretna wartość.

let user1 = {name : "Cezary"};
let user2 = {name : "Kasia"};

let users = new Map();

users.set(user1, "Programista");
users.set(user2),"HR";

console.log(users.get(user1));

!_07.png

W konsoli pojawi się wartość, która jest pod pierwszym kluczem. Jest to "Programista".

Oto dokładnie ten sam przykład tylko po dodaniu rekordów usunę jeden element, a potem sprawdzę rozmiar swojej mapy. 

let user1 = {name : "Cezary"};
let user2 = {name : "Kasia"};

let users = new Map();

users.set(user1, "Programista");
users.set(user2),"HR";

users.delete(user1);
console.log(users.size);

!_08.png

Dostanę informację o tym, że wielkość mojej mapy równa się 1. Istnieje alternatywny sposób dodawania elementów do mapy. Możesz stworzyć tablice z tablicami i później JavaScript odpowiednio przełoży to na obiekt Map.

let user1 = {name : "Cezary"};
let user2 = {name : "Kasia"};

let arr = [
    [user1,"Programista"],
    [user2,"HR"]
];

let users = new Map(arr);
console.log(users.size);

!_09.png

W tym wypadku rozmiar naszej mapy jest równy 2.

Jakby chciał zdobyć wszystkie wartości przechowane w mapie, to jak bym to zrobił? Używając operatora "spread" mógłbym utworzyć listę wartość w taki sposób.

let user1 = {name : "Cezary"};
let user2 = {name : "Kasia"};

let arr = [
    [user1,"Programista"],
    [user2,"HR"]
];

let users = new Map(arr);
let list = [...users.values()];
console.log(list);

!_10.png

W konsoli pojawi się tablica:

["Programista","HR"]

Mogę też całą mapę przy pomocy funkcji "entries()" rozbić tablice tablic i wyciągnąć klucze i wartości, jakie mi się podobają. Taki kod da wynik:

let user1 = {name : "Cezary"};
let user2 = {name : "Kasia"};

let arr = [
    [user1,"Programista"],
    [user2,"HR"]
];

let users = new Map(arr);
let list = [...users.entries()];
console.log(list[0][1]);
console.log(list[1][0]);

Programista

{name : "Kasia"}

Co się stanie, gdy wyzeruje swoje wskaźniki do kluczy? Czy te wartości znikną z Mapy?

let user1 = {name : "Cezary"};
let user2 = {name : "Kasia"};

let arr = [
    [user1,"Programista"],
    [user2,"HR"]
];

let users = new Map(arr);
user1 = null;
user2 = null;

console.log(users.size);

W tym wypadku nie. Liczba elementów w Map zostanie taka sama i wciąż one tam istnieją.

Teraz zobaczmy jako, to działa z WeakMap.

let user1 = {name : "Cezary"};
let user2 = {name : "Kasia"};

let arr = [
    [user1,"Programista"],
    [user2,"HR"]
];

let users = new WeakMap(arr);
user1 = null;
user2 = null;

console.log(users.size);

Po pierwsze te wygibasy ze zmiennymi null nie mają znaczenia na działanie tego kodu. WeakMap nie ma właściwości "size" wiec otrzymasz wartość undefined. Po drugie nawet nie możesz zobaczyć, co żyje w WeakMap, ponieważ nie możesz zrobić iteracji.

Możemy tylko założyć, że w momencie, gdy nasze klucze stały się NULL, sinik JavaScript posprzątał referencję do tych obiektów i tak one nie istnieją już w WeakMap. Chociaż fakt nie możesz tego sprawdzić.

Set i WeakSet

Set i WeakSet są podobnymi kolekcjami do Map i WeakMap. Zajmują się one jednak pojedynczymi wartościami lub obiektami. Nie ma tutaj klucza. 

Rolą tych kolekcji jest zagwarantowanie unikatowości wartości. 

Jak się domyślasz WeakSet działa podobnie jak WeakMap. Jak dokładnie to zaraz zobaczymy.

Oto prosty przykład użycia Set.

let games = new Set();

games.add('Doman');
games.add('Franko');
games.add('Mentor');

console.log(games.size);

Dodaliśmy 3 elementy, więc w konsoli zostanie zwrócona liczba 3.Zobaczmy, co się stanie, gdy spróbuje dodać dwa razy tę samą wartość. Jak myślisz, ile ma elementów w tym Set.

let games = new Set();

games.add('Doman');
games.add('Franko');
games.add('Doman');

console.log(games.size);

Konsola powie mi, że ta kolekcja ma 2 elementy. Set pilnuje i nie pozwoli dodać dwa razy tej samej wartości do kolekcji. 

Mogę uzupełnić, tę kolekcję dodają od razu do nie tablice wartości.

let games = new Set([
 'Doman',
 'Franko',
 'Misja Harolda'
]);

console.log(games.size);

Mogę używając funkcji "has()" - sprawdzić jakie elementy ma Set.

let games = new Set([
 'Doman',
 'Franko',
 'Misja Harolda'
]);

console.log(games.has('Doman'));
console.log(games.has('Mentor'));

Dostane wartość true na początku, ponieważ gra Doman istnieje. Natomiast później dostanę wartość false, gdyż w Set nie mam gry Mentor.

Tak jak w klasie Map mam tutaj dostęp do funkcji keys(), values(), entries(). Jak myślisz co dostane do konsoli.

let games = new Set([
 'Franko',
 'Misja Harolda'
]);

console.log(...games.keys());
console.log(...games.values());
console.log(...games.entries());

Kluczy w Set nie ma przecież kluczy. Co więc zostanie zwrócone. W funkcji keys() otrzymasz tablicę wartości. W funkcji values() otrzymasz też wartości. Natomiast dla entries() otrzymasz:

(2) ["Franko", "Franko"] (2) ["Misja Harolda", "Misja Harolda"]

Czyli jak widzisz, wartości dla Set są równocześnie kluczami.

Sprawdźmy, jeszcze jak działa ta unikatowość Set. Co się stanie jak trzy identyczne obiekty. Ile mam ostatecznie elementów w Set.

let posts = new Set([
    {id :605},
    {id :605},   
    {id :605},
]);
console.log(posts.size);

Dostaniemy wartość 3, mimo iż obiekty są identyczne, to wciąż są to oddzielne obiekty. A co powiesz na to? Czy JavaScript potraktuje te wyrażenia jako te same wartości? W końcu przy odpowiednim rzutowaniu wartości w JavaScript 0 i '0' oraz false mogą reprezentować to samo. Co pojawi się w konsoli?

let posts = new Set([
    0,
    '0',   
    false,
]);
console.log(posts.size);

Dostaniesz oczywiście wartość 3. Prymitywne wartości mogą być umieszczone w Set i jak widzisz wszystkie, te wartości zostały uznane za różne. 

Jak działa WeakSet? Na początku zobaczmy czy możemy określić rozmiar WeakSet.

let games = new WeakSet([
 'Franko',
 'Misja Harolda'
]);

console.log(games.size);

Dostaniesz błąd, który wynika z tego, że nie można dodać do WeakSet typów prymitywnych.

Dodajmy więc bardziej złożone obiekty. Co teraz pojawi się w konsoli? 

let p1 = {name:'Doman'};
let p2 = {name:'Franko');

let games = new WeakSet([p1,p2]);

console.log(games.size);

Tak jak w WeakMap nie ma dostępu do właściwości size, więc nie możesz określić, jaki rozmiar ma WeakSet. Czy taki kod jest poprawny?

let p1 = {name:'Doman'};
let p2 = {name:'Franko');

let games = new WeakSet([p1,p2]);

console.log(games.has(p1));

Tak, dopóki masz oryginale obiekty, które dodałeś, to możesz sprawdzić, czy znajdują się one w WeakSet.

let p1 = {name:'Doman'};
let p2 = {name:'Franko');

let games = new WeakSet([p1,p2]);

p1 = null;
console.log(games.has(p1));

Gdy pozbędziesz się już tego obiektu, to wtedy możesz już uznać, że go WeakSet go nie ma. Co prawda ten kod tego nie testuje, ale chciałem Ci pokazać, że WeakSet jest bardzo słabo powiązany obiektami.

Dziedziczenie po tablicy

Tablica w JavaScript jest klasą, a to oznacza, że można po niej dziedziczyć. W sumie to dużo rzeczy możesz rozszerzyć w JavaScript: Boolean, Number, String, Map, Set, Function, Promise, RegExp.

Problem polega jednak na tym, że nie każda przeglądarka czy transpilator/kompilator wspiera taki bajer. Dziedziczenie po gotowych klasach w języku brzmi jak bardzo złożona procedura, która musi zostać zbudowana w przeglądarce albo w transpilatorze. 

Jak widzisz Babel 7 i TypeScript nie wspiera takiego zachowania.

dziedziczenie.PNG

Mimo wszystko zobaczmy, jak to możesz zrobić. Czy taki kod jest poprawny?

class Games extends Array {
}

let a = Games.from(
['Blitz Bombers','Shadow Of the Beast','Cannon Fodder']);

console.log(a instanceof Games);

Tak. Moja klasa Games ma funkcję from(), ponieważ dziedziczy ona po tablicy.

Czy nowa utworzona tablica z mojej klasy będzie instancją mojej klasy?

class Games extends Array {
}

let a = Games.from(
['Blitz Bombers','Shadow Of the Beast','Cannon Fodder']);
let newArray = a.reverse();

console.log(newArray  instanceof Games);

Odpowiedź brzmi tak.

Pora pokazać jakiś przykład, by pokazać, że dziedziczenie po Array ma jakieś zastosowanie. Możesz w końcu napisać swoje własne funkcje do swojej klasy.

Mamy tutaj dwie funkcje. Pierwsza zwróci grę, która zaczyna się od konkretnej litery. Druga metoda zwróci tablicę obiektów. Te obiekty będą miały właściwość letter określający pierwszy znak w tytule gry oraz samą grę. Co otrzymam w konsoli?

class Games extends Array {

    getByStartLetter(chart) {

        return this.filter(x => x[0] === chart);
    }

    getDictionary() {
       var letters = this.map((game) => 
            ({letter:game[0],game:game}));
       return letters;
    }
}

let a = Games.from(
['Blitz Bombers','Shadow Of the Beast','Cannon Fodder']);

console.log(a.getByStartLetter('S'));
console.log(a.getDictionary());

Otrzymam:

Games ["Shadow Of the Beast"]

Games(3) [{…}, {…}, {…}]

0: {letter: "B", game: "Blitz Bombers"}

1: {letter: "S", game: "Shadow Of the Beast"}

2: {letter: "C", game: "Cannon Fodder"}

Podsumowanie

W tym wpisie spojrzeliśmy na tablice i inne kolekcje w JavaScript. Ten cykl się nie kończy,