Obiekty w JavaScript to podstawowe struktury danych służące do przechowywania kolekcji właściwości powiązanych z wartościami. Właściwość (property) to para klucz-wartość. Kluczem jest zazwyczaj string (lub symbol), a wartością dowolny typ danych (liczba, tekst, funkcja, inny obiekt, itd.). Obiekty pozwalają modelować bardziej złożone byty, jak np. użytkownik, samochód, ustawienia aplikacji itp., grupując powiązane informacje pod jedną nazwą. W tej sekcji nauczymy się tworzyć obiekty, odczytywać i zmieniać ich właściwości, definiować metody (funkcje będące właściwościami obiektu) i zrozumiemy, jak działa słowo kluczowe this
. Omówimy też zagnieżdżanie obiektów oraz niektóre operacje na obiektach (iteracja, kopiowanie, łączenie).
Tworzenie i właściwości obiektu
Najprostszym sposobem utworzenia obiektu jest użycie literału obiektu, czyli par klucz:wartość ujętych w nawiasy klamrowe { }
. Przykładowo:
const person = {
name: "Jan",
age: 30
};
Powyżej zdefiniowaliśmy obiekt person
z dwoma właściwościami:
- klucz
"name"
z wartością"Jan"
(łańcuch znaków), - klucz
"age"
z wartością30
(liczba).
Klucze są tutaj napisami (JS automatycznie traktuje identyfikatory właściwości jako stringi, o ile są poprawnymi identyfikatorami). Można też ująć klucz w cudzysłów explicite, zwłaszcza gdy zawiera spacje lub znaki specjalne, np. {"full name": "Jan Kowalski"}
byłoby poprawne (choć do takiej właściwości trudniej się odwołać przez kropkę, o czym za moment). Wartości mogą być różnych typów – w tym inne obiekty lub funkcje.
Obiekt możemy też stworzyć za pomocą konstruktora new Object()
i potem dynamicznie dodawać właściwości, ale literał { }
jest krótszy i częściej używany.
Dostęp do właściwości obiektu
Do właściwości obiektu w JavaScript odwołujemy się na dwa sposoby:
- Notacja kropkowa –
obiekt.klucz
- Notacja nawiasowa –
obiekt["klucz"]
Kontynuując przykład obiektu person
:
console.log(person.name); // "Jan"
console.log(person.age); // 30
// To samo za pomocą notacji nawiasowej:
console.log(person["name"]); // "Jan"
Notacja kropkowa jest wygodna i często używana, ale ma ograniczenie: działa tylko z takimi nazwami właściwości, które są poprawnymi identyfikatorami w JavaScript (czyli nie zawierają spacji, nie zaczynają się od cyfry itp.). Gdy nazwa właściwości jest nietypowa, albo jest w zmiennej, należy użyć notacji nawiasowej. Np.:
person["full name"] = "Jan Kowalski"; // dodajemy nową właściwość ze spacją w nazwie
console.log(person["full name"]); // "Jan Kowalski"
const prop = "age";
console.log(person[prop]); // 30 (odwołanie poprzez nazwę w zmiennej)
W powyższym kodzie pokazaliśmy dwa przypadki, gdzie notacja []
jest niezbędna: gdy klucz zawiera spację oraz gdy klucz jest przechowywany w zmiennej (prop
). Notacja nawiasowa przyjmuje ciąg znaków (string) lub symbol jako klucz i zwraca odpowiadającą wartość (lub pozwala ustawić wartość).
Jeśli spróbujemy odczytać właściwość, która nie istnieje, otrzymamy undefined
. Można to sprawdzić operatorem in
(np. "age" in person
zwróci true
, "salary" in person
zwróci false
) lub porównując do undefined
.
Metody obiektu i słowo kluczowe this
Jeśli jako wartość właściwości obiektu przypiszemy funkcję, nazywamy taką właściwość metodą obiektu. Metody pozwalają obiektom na posiadanie „zachowań”, operacji związanych z nimi. Np. obiekt person
może mieć metodę greet
wypisującą powitanie.
Przykład dodania metody do obiektu:
person.greet = function() {
console.log("Cześć, jestem " + this.name + "!");
};
person.greet(); // Cześć, jestem Jan!
Tutaj dodaliśmy właściwość greet
, która jest funkcją. Wywołujemy ją poprzez person.greet()
. Zauważ użycie słowa kluczowego this
wewnątrz metody. this
odnosi się do bieżącego obiektu, na którym metoda została wywołana. W naszym przypadku wywołanie person.greet()
sprawia, że wewnątrz funkcji greet
słowo this
wskazuje na obiekt person
. Dlatego this.name
w środku funkcji zwróci person.name
(„Jan”). W efekcie metoda wypisuje komunikat ze wskazaniem imienia obiektu.
Kilka uwag dotyczących metod i this
:
- Możemy definiować metody od razu przy tworzeniu obiektu literatem:
const calculator = {
x: 5, y: 3, sum: function() {
return this.x + this.y;
}
};
console.log(calculator.sum()); // 8
W powyższym przykładzie obiekt calculator
ma właściwości liczbowe x
i y
oraz metodę sum
zwracającą ich sumę. this.x
odnosi się do calculator.x
.
- Istnieje skrócona składnia zapisu metod w literałach obiektów (ES6): zamiast
sum: function() { ... }
można napisaćsum() { ... }
– efekt jest ten sam, krótszy zapis. this
zawsze zależy od kontekstu wywołania funkcji. Jeżeli oddzielimy funkcję od obiektu, np.const tylkoFunkcja = person.greet;
i wywołamytylkoFunkcja()
, to w środku tej funkcjithis
nie będzie już wskazywał naperson
(w trybie strict będzieundefined
, a w trybie niestrict wskazywałby na obiekt globalny). Dlatego ważne jest, jak wywoływana jest metoda – czy jakoobiekt.metoda()
, czy samodzielnie.- Funkcje strzałkowe, jak wspomniano wcześniej, nie mają własnego
this
. Gdybyśmy zdefiniowaligreet
jako funkcję strzałkową:greet: () => { console.log(this.name) }
wewnątrzperson
, tothis
w niej byłby rozwiązywany leksykalnie – zapewne wskazywałby nawindow
lubundefined
(jeśli używamy modułów/strict mode), a nie na obiektperson
. Dlatego do metod obiektu najlepiej używać zwykłych funkcji, abythis
wskazywało na obiekt.
Modyfikacja i zagnieżdżone obiekty
Obiekty w JavaScript są mutowalne, co oznacza, że po utworzeniu obiektu możemy dodawać, usuwać czy zmieniać jego właściwości w trakcie działania programu.
- Dodawanie/edytowanie właściwości: Odbywa się to poprzez proste przypisanie wartości do klucza, używając notacji kropkowej lub nawiasowej.
person.job = "Programista"; // dodajemy nową właściwość job
person.age = 31; // edytujemy istniejącą właściwość age
Po tych operacjach obiekt person
ma dodatkowo właściwość job
oraz zmienioną wartość age
.
- Usuwanie właściwości: Służy do tego operator
delete
.
delete person["full name"]; // usuwa właściwość 'full name' z obiektu
delete person.job; // usuwa właściwość 'job'
Po użyciu delete
danej właściwości już nie będzie w obiekcie (uwaga: operator ten działa tylko na własne właściwości obiektu; nie usuwa ewentualnych właściwości dziedziczonych po prototypie).
Obiekty mogą być zagnieżdżone, tzn. właściwości obiektu mogą same być obiektami (lub tablicami). Pozwala to tworzyć struktury drzewiaste reprezentujące bardziej złożone dane. Na przykład:
const company = {
name: "ABC Corp",
address: {
city: "Warszawa",
zip: "00-001",
},
employees: [
{ name: "Jan", position: "CEO" },
{ name: "Anna", position: "Developer" }
]
};
console.log(company.address.city); // "Warszawa"
console.log(company.employees[1].name); // "Anna"
W powyższym obiekcie company
właściwość address
jest obiektem z kolejnymi własnymi właściwościami city
i zip
. Z kolei employees
jest tablicą zawierającą obiekty reprezentujące pracowników. Dostęp do zagnieżdżonych danych uzyskujemy przez łańcuch notacji kropkowej/nawiasowej: np. company.employees[1].name
najpierw pobiera drugi element tablicy employees
(o indeksie 1), a następnie jego pole name
.
Zagnieżdżone obiekty modyfikujemy podobnie jak płaskie:
company.address.street = "Ul. Długa 5"; // dodajemy ulicę do adresu
company.employees[0].position = "CTO"; // zmieniamy stanowisko pierwszego pracownika
Trzeba tylko uważać, by odwołując się do głębokich właściwości, mieć pewność że po drodze żaden „poziom” nie jest undefined
(w przeciwnym razie dostaniemy błąd typu Cannot read property ... of undefined
). W nowszych wersjach JS istnieje opcja bezpiecznego dostępu łańcuchowego ?.
(optional chaining), ale to temat na inny raz.
Operacje na obiektach
Poza bezpośrednim odczytem i zapisem właściwości, często potrzebujemy wykonać różne operacje ogólne na obiekcie:
- Iteracja po właściwościach: Do przechodzenia po wszystkich własnych właściwościach obiektu można użyć pętli for…in.
for (let key in person) {
if (person.hasOwnProperty(key)) {
console.log(key, ":", person[key]);
}
}
Pętla for...in
iteruje po kluczach (nazwach właściwości) obiektu person
. Użycie hasOwnProperty
zapewnia, że pomijamy ewentualne właściwości dziedziczone z prototypu. Wewnątrz pętli możemy użyć person[key]
, by uzyskać wartość danej właściwości.
Uwaga: Kolejność iteracji for...in
nie jest gwarantowana (choć w praktyce przeglądarki zwykle zwracają w kolejności dodawania dla zwykłych właściwości liczbowych i potem nieliczbowych).
- Pobranie listy kluczy, wartości: Obiekt
Object
dostarcza metody statyczne do tego celu:Object.keys(person)
zwróci tablicę wszystkich kluczy (np.["name","age","greet"]
),Object.values(person)
zwróci tablicę wszystkich wartości (np.["Jan", 31, function]
),Object.entries(person)
zwróci tablicę par [klucz, wartość] (np.[ ["name","Jan"], ["age",31], ["greet", ƒ] ]
).
Te metody są wygodne, gdy chcemy np. policzyć właściwości lub przekształcić obiekt w inną strukturę. - Kopiowanie obiektów: Ponieważ obiekty są przekazywane przez referencję, proste przypisanie
const obj2 = obj1;
nie tworzy kopii, a jedynie nowy odnośnik do tego samego obiektu. Aby skopiować obiekt (płytka kopia, tzw. shallow copy), można użyć:- operatora spread:
const kopia = {...person};
– tworzy nowy obiekt, rozpakowując wszystkie własnościperson
do niego, - lub
Object.assign
:const kopia = Object.assign({}, person);
– efekt ten sam co spread.
Obie te metody skopiują wartości właściwości, ale jeśli któraś z nich jest obiektem zagnieżdżonym, to skopiuje się tylko referencja (czyli kopia będzie współdzielić z oryginałem zagnieżdżony obiekt). To właśnie oznacza kopia płytka. Głębokie kopiowanie wymaga dodatkowych technik (np. rekurencji, funkcji z biblioteki, czy pośrednio JSON.stringify/parse, z pewnymi ograniczeniami).
- operatora spread:
- Łączenie/rozszerzanie obiektów: Operator spread
{...obj1, ...obj2}
można też użyć do połączenia własności dwóch obiektów w jeden. Jeśli występują te same klucze, własność z obiektu po prawej stronie nadpisze tę z lewej. Przykład:
const objA = { x: 1, y: 2 };
const objB = { y: 3, z: 4 };
const objC = { ...objA, ...objB };
console.log(objC); // { x: 1, y: 3, z: 4 }
W wyniku objC
zawiera kombinację objA
i objB
. Własność y
została nadpisana wartością z objB
.
Alternatywnie można użyć Object.assign(objA, objB)
, co dołączy objB
do objA
(modyfikując objA
).
- Inne operacje: Sprawdzanie istnienia właściwości (
"key" in obj
lubobj.hasOwnProperty("key")
), usuwanie właściwości (delete obj.key
– już omówione), blokowanie zmian obiektu (Object.freeze(obj)
aby uczynić obiekt niezmiennym), itp. Te zaawansowane operacje są przydatne w określonych sytuacjach, ale wykraczają poza podstawy.
Podsumowując, obiekty w JavaScript są wszechstronne i można je dynamicznie tworzyć oraz modyfikować. Ponieważ są przekazywane przez referencję, należy być ostrożnym przy ich kopiowaniu i porównywaniu (porównanie dwóch różnych obiektów nawet o identycznej zawartości zwróci false, bo porównują się referencje, nie wartości).