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:

  1. Primitive types, czyli typy proste
    Do typów prostych zaliczamy: number, string, bigint, boolean, null, undefined oraz symbol.
  2. 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.

Typ prosty - primitive type
Typ prosty

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.

Typ referencyjny - reference type
Typ referencyjny

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