Dzisiejszy post będzie dotyczył głównie podstaw JavaScriptu (chociaż temat dotyczy też innych języków), ale również pozwoli na zrozumienie, czym są typy proste oraz czym różni się przekazywanie wartości przez referencję. W różnych językach może to odbywać się inaczej. Na przykład w inny sposób przekazywana jest tablica w JavaScript, a inaczej w języku PHP.
Artykuł ten celowo umieszczam na blogu, bo przyda się nam do kolejnej publikacji, która zostanie opublikowana za 2 tygodnie, a będzie dotyczyła programowania funkcyjnego. Dokładne zrozumienie przekazywania wartości do funkcji i modyfikacji danych, pozwoli nam pisać funkcje zgodnie z paradygmatem programowania funkcyjnego.
Podział typów
W języku JavaScript typy możemy podzielić na dwa rodzaje:
- Primitive types, czyli typy proste
Do typów prostych zaliczamy: number, string, bigint, boolean, null, undefined oraz symbol. - Reference types, typy referencyjne
Do typów referencyjnych zaliczamy: array, object oraz function, czyli nie trudno się domyślić – tablicę, obiekt oraz funkcję.
Różnica w przekazywaniu wartości
Aby w bardziej przyziemny sposób przedstawić na czym polega różnica w przekazywaniu typów prostych oraz typów referencyjnych, posłużę się pewną metaforą…
Przypisując typ prosty do zmiennej możemy myśleć o tej zmiennej jako adresie, który prowadzi nas do wskazanej wartości.
Kiedy będziemy przypisywać wcześniej utworzoną zmienną do innej zmiennej (let a = b
), a następnie zmodyfikujemy jedną z nich, dane będą modyfikowane niezależnie. Finalnie otrzymamy dwie zmienne, które będą kierowały do dwóch różnych wartości.
W przypadku referencji, sprawa ma się trochę inaczej. Przypisując typ referencyjny do zmiennej również możemy myśleć o tej zmiennej jako adresie, który prowadzi nas do tej wartości.
W sytuacji, gdy zmienną (która zawiera typ referencyjny), przypiszemy do innej zmiennej, a następnie zmodyfikujemy jedną z nich, będziemy nadal modyfikować tę samą funkcję/obiekt/tablicę. Finalnie posiadamy dwie zmienne, które prowadzą do tego samego obiektu/tablicy/funkcji.
W przypadku referencji, zmienna two
kieruje nadal do tej samej tablicy.
Przejdźmy jednak do przykładów, ponieważ nadal może być to niezbyt klarowne.
Na początek zobaczmy, w jaki sposób sprawdzić typ zmiennej w JavaScript.
let a = 2; let b = 'test'; let c = {}; console.log(typeof(a)); // number console.log(typeof(b)); // string console.log(typeof(c)); // object
A teraz prześledźmy przykłady i różnice w przekazywaniu danych poprzez wartość oraz referencję.
let value = 'Lukasz'; console.log(value); // Lukasz let value_two = value; console.log(value_two); // Lukasz value = 'Magicweb.pl'; console.log(value); // Magicweb.pl console.log(value_two); // Lukasz
Powyższy przykład opiera się na typie prostym – stringu. value
oraz value_two
możemy modyfikować niezależnie. Zmieniając wartość w value
, value_two
pozostaje niezmieniona (i odwrotnie). Przypisując value
do value_two
w linii 3, niejako „kopiujemy wartość”.
Poniżej – podobna sytuacja, jednak tym razem będziemy działać na obiekcie, czyli typie referencyjnym.
let object_one = { name: 'Lukasz' }; console.log(object_one.name); // Lukasz let object_two = object_one; console.log(object_two.name); // Lukasz object_one.name = 'Magicweb.pl'; console.log(object_one.name); // Magicweb.pl console.log(object_two.name); // Magicweb.pl object_two.name = 'Change name'; console.log(object_one.name); // Change name console.log(object_two.name); // Change name
Przeanalizujmy powyższy przykład. Stworzyliśmy obiekt object_one
i do object_two
przypisaliśmy object_one
. Zarówno object_one.name
jak i object_two.name
zwraca wartość Lukasz, co oczywiście jest naturalne. W kolejnej sekcji modyfikujemy object_one.name
, zmieniając wartość z Lukasz na Magicweb.pl. W tym momencie dzieje się jednak magia referencji. Zarówno object_one
jak i object_two
, dla właściwości name
posiada wartość Magicweb.pl, ponieważ object_two
oraz object_one
jest referencją do tego samego obiektu. Ta sama sytuacja wystąpi, jeżeli będziemy chcieli zmodyfikować object_two
, co pokazuje nam trzecia sekcja kodu.
Array – reference type
let array_one = []; let array_two = array_one; array_one.push('a'); array_two.push('b'); console.log(array_one); // [ 'a', 'b' ] console.log(array_two); // [ 'a', 'b' ]
W przypadku tablicy, zmienna array_one
oraz array_two
będzie referencją do tej samej tablicy, dlatego w przypadku wywołania operacji push()
, nadal modyfikujemy jedną i tą samą tablicę.
Porównywanie wartości i zmiennych (== oraz ===)
Jeżeli już mówimy o przekazywaniu wartości, muszę też wspomnieć o porównywaniu typów prostych oraz typów referencyjnych. W przypadku typów prostych wygląda to bardzo prosto.
let one = 'hello'; let two = 'hello'; console.log(one == two); // true console.log(one === two); // true
W przypadku typów referencyjnych, sprawa wygląda inaczej, co pokazują poniższe przykłady.
let one = ['test']; let two = ['test']; console.log(one == two); // false console.log(one === two); // false
W przypadku typów referencyjnych, nawet jeżeli typ referencyjny zawiera pozornie te same wartości, porównanie zwróci false
. Dzieje się tak dlatego, że w tym przypadku porównywana jest referencja.
let one = ['test']; let two = one; console.log(one == two); // true console.log(one === two); // true
Pojawia się zatem pytanie, jak w bardzo prosty sposób sprawdzić, czy tablica / obiekt jest „taki sam”? Z pomocą przyjdzie nam JSON.stringify()
.
let one = ['test']; let two = ['test']; let oneStringify = JSON.stringify(one); let twoStringify = JSON.stringify(two); console.log(oneStringify === twoStringify); // true
let one = {name: 'Magicweb'}; let two = {name: 'Magicweb'}; let oneStringify = JSON.stringify(one); let twoStringify = JSON.stringify(two); console.log(oneStringify === twoStringify); // true
Trzeba jednak pamiętać, że kolejność właściwości czy też wartości będzie miała znaczenie i JSON.stringify()
, zwróci false
, jeżeli kolejność nie zostanie zachowana.
let one = {value: 'test', name: 'Magicweb'}; let two = {name: 'Magicweb', value: 'test'} let oneStringify = JSON.stringify(one); let twoStringify = JSON.stringify(two); console.log(oneStringify === twoStringify); // false
Możemy też napisać własną funkcję, która sprawdza wartości wewnątrz funkcji/obiektu i wtedy sami zdecydujemy, czy kolejność jest dla nas istotna, czy też nie.
PS. O przypisywaniu wartości i zarządzaniu pamięcią można przeczytać tutaj: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Memory_Management
Dziękuję, że udało się dotrzeć do końca i zapraszam do następnych artykułów. 🙂
Na koniec chciałbym też podziękować mojemu koledze, który słusznie zauważył, że pewien zapis z pierwszej wersji artykułu mógł wprowadzić czytelnika w błąd (artykuł został zaktualizowany) Maciek prowadzi kurs https://cotenfrontend.pl/ zatem w ramach podziękowania postanowiłem polecić jego projekt. 🙂
Źródła:
Obrazy – typ prosty / typ referencyjny: draw.io
Obraz: https://unsplash.com/photos/w7ZyuGYNpRQ