W tym epizodzie poznamy podstawy nowego typu danych, jakim jest obiekt, nauczymy się jak go tworzyć, dokonywać dekompozycji obiektu, poznamy podstawowe funkcje związane z obiektami, nauczymy się pisać funkcje-konstruktory oraz inne funkcje zwracające obiekt.
Obiekt to takie połączenie słownika/haszmapy oraz klas, znanych z innych języków programowania.
Obiekt zawiera pary klucz-wartość, może również zawierać metody. Znamy już typy liczbowe, napisowe, oraz typ tablicy, najwyższa pora poznać obiekty.
W ramach ciekawostki dodam, że tak „pod spodem” to każdy typ w JS jest obiektem. Nas natomiast będą interesować obiekty w rozumieniu znajdujące się w nawiasach klamrowych zbiory par klucz-wartość oraz metod, które sami zdefiniujemy.
Jak zwykle, podaję templatkę pliku HTML, który utworzymy, otworzymy edytorem tekstowym (dowolnym, ale polecam VSCode do programowania), będziemy tam pisać kod wewnątrz tagów <script> zaś obserwować działanie naszego kodu będziemy używając przeglądarki internetowej i otwierając nasz plik html w ten sposób.
Templatka:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<h1>Welcome to my website</h1>
<script>
//nasz kod tutaj
</script>
</body>
</html>
Literał obiektu – tworzenie i dekompozycja
Obiekty tworzymy używając literału obiektu, czyli nawiasów klamrowych i wewnątrz nich oddzielonych przecinkiem par klucz-wartość. Klucz od wartości oddzielamy znakiem dwukropka:
<script>
let person = {
name: "John",
age: 30
}
</script>
Do właściwości obiektu możemy się odnieść poprzez jego nazwę, znak kropki i nazwę klucza:
<script>
let person = {
name: "John",
age: 30
}
console.log(person.name);
//John
console.log(person.age);
//30
</script>
Obiekty możemy tak samo jak tablice poddawać dekompozycji, czyli „wpakować” ich wartość do jakiejś zmiennej:
<script>
let person = {
name: "John",
age: 30
}
let {name, age} = person;
console.log(name);
console.log(age);
//John
//30
</script>
Tutaj wyciągamy właściwości name oraz age z obiektu person i zapisujemy w tak samo nazwanej zmiennej, do której dostęp mamy.
Możemy też wyciągnąć tylko jedną, chcianą przez nas wartość:
<script>
let person = {
name: "John",
age: 30
}
let {age} = person;
console.log(age);
//30
</script>
Zmienne, do których zapisujemy wartości z obiektu możemy nazwać po swojemu, inaczej niż klucze tego obiektu:
<script>
let person = {
name: "John",
age: 30
}
let {name : personName, age : personAge} = person;
console.log(personName);
//John
console.log(personAge);
//30
</script>
Tutaj wyciągnęliśmy wartości name oraz age i zapisaliśmy do zmiennych o nazwach personName i personAge.
Metody obiektu – różne sposoby
Do obiektu możemy przypisywać metody na różne sposoby. Pierwszy wygląda tak:
<script>
let person = {
name: "John",
age: 30,
sayHi: function() {
console.log("HI!")
}
}
person.sayHi();
//HI!
</script>
Jak widać, do metod (czyli funkcji obiektu) odnosimy się po kropce. Od pewnego czasu istnieje możliwość łatwiejszego zapisu metod, bez słowa function:
<script>
let person = {
name: "John",
age: 30,
sayHi() {
console.log("HI!")
}
}
person.sayHi();
//HI!
</script>
Mamy jeszcze kolejny sposób, czyli dopisanie metody poza literałem obiektu:
<script>
let person = {
name: "John",
age: 30,
}
person.sayHi = function() {
console.log("HI!");
}
person.sayHi();
//HI!
</script>
Nie tylko metody możemy dopisywać w ten sposób, także proste klucze i wartości:
<script>
let person = {
name: "John",
age: 30,
}
person.gender = "Male";
console.log(person.name);
console.log(person.age);
console.log(person.gender);
//John
//30
//Male
</script>
Metody obiektu – słówko kluczowe 'this’
Słówko kluczowe 'this’ pozwala tworzyć metody odnoszące się do pól danego obiektu. Łatwiej to zobrazować na przykładzie.
<script>
let person = {
name: "John",
age: 30,
introduce() {
console.log(`My name is ${this.name}`);
},
sayAge() {
console.log(`I am ${this.age} years old`);
}
}
person.introduce();
//My name is John
person.sayAge();
//I am 30 years old
</script>
Jak widać this odnosi się do danego obiektu, jego pól. Słówko to może także wywoływać inne metody danego obiektu:
<script>
let person = {
name: "John",
age: 30,
introduce(age=false) {
console.log(`My name is ${this.name}`);
if(age === true)
this.sayAge();
},
sayAge() {
console.log(`I am ${this.age} years old`);
}
}
person.introduce();
//My name is John
person.introduce(true);
//My name is John
//I am 30 years old
</script>
Jak widać metoda introduce przyjmuje teraz jeden argument age z domyślną wartością false. Jeżeli jednak do introduce przekażemy ten argument jako true, metoda introduce wywoła inną metodę, sayAge.
Słówko this pozwala nam odnosić się wewnątrz obiektu do jego pól i metod.
Możemy nawet pola tego obiektu modyfikować:
<script>
let person = {
name: "John",
age: 30,
introduce(age=false) {
console.log(`My name is ${this.name}`);
if(age === true)
this.sayAge();
},
sayAge() {
console.log(`I am ${this.age} years old`);
},
getOlder() {
this.age += 1;
}
}
person.introduce(true);
//My name is John
//I am 30 years old
person.getOlder();
person.introduce(true);
//My name is John
//I am 31 years old
</script>
Tutaj metoda getOlder zwiększa wartość 'age’ o 1. Dostęp do tego uzyskujemy poprzez słówko kluczowe 'this’.
Obiekt płynny – return this
Jeżeli rzucimy sobie okiem na nasz ostatni przykład, zauważymy, że metody, które dodaliśmy niczego nie zwracają. Oczywiście możemy sobie dodać metodę, która akurat coś zwróci:
<script>
let person = {
name: "John",
age: 30,
introduce(age=false) {
console.log(`My name is ${this.name}`);
if(age === true)
this.sayAge();
},
sayAge() {
console.log(`I am ${this.age} years old`);
},
getOlder() {
this.age += 1;
},
getAge() {
return this.age;
}
}
console.log(person.getAge());
//30
</script>
Metoda getAge zwraca tutaj wiek. Możemy to przypisać do zmiennej albo tak jak w przykładzie – wrzuć to w funkcję console.log.
Nic nie stoi jednak na przeszkodzie, aby funkcje, które nic nie zwracają, zwracały nasz obiekt poprzez użycie 'return this’
<script>
let person = {
name: "John",
age: 30,
introduce(age=false) {
console.log(`My name is ${this.name}`);
if(age === true)
this.sayAge();
return this;
},
sayAge() {
console.log(`I am ${this.age} years old`);
return this;
},
getOlder() {
this.age += 1;
return this;
},
getAge() {
return this.age;
}
}
console.log(person.getAge());
//30
</script>
Po co mielibyśmy to robić? Uporządkujmy sobie nieco nasz obiekt, ponieważ rozrósł się ponad miarę:
<script>
let person = {
name: "John",
age: 30,
introduce() {
console.log(`My name is ${this.name}`);
return this;
},
sayAge() {
console.log(`I am ${this.age} years old`);
return this;
},
getOlder() {
this.age += 1;
return this;
},
getAge() {
return this.age;
}
}
console.log(person.getAge());
//30
</script>
Teraz, mając funkcje, które zawsze zwracają obiekt, możemy w płynny sposób używać tych funkcji jedna po drugiej w bardzo prosty sposób, jako że one na końcu ten obiekt zwracają.
<script>
let person = {
name: "John",
age: 30,
introduce() {
console.log(`My name is ${this.name}`);
return this;
},
sayAge() {
console.log(`I am ${this.age} years old`);
return this;
},
getOlder() {
this.age += 1;
return this;
},
getAge() {
return this.age;
}
}
person.introduce().sayAge();
//My name is John
//I am 30 years old
person.getOlder().getOlder().getOlder();
person.introduce().sayAge();
//My name is John
//I am 33 years old
</script>
Jak widać, używamy sobie tych metod na obiekcie jedna po drugiej – i możemy to robić tak długo, jak nasze metody zwracają 'this’, czyli ten obiekt.
Oczywiście zbyt długie linijki tego typu mogą być mało czytelne, natomiast teoretycznie możemy to wszystko zapisać w jednej linijce, po kropce wywołując kolejne metody.
Tak długo, jak korzystamy z metod zwracających 'this’ możemy to robić w nieskończoność, ponieważ metoda ta wykonuje jakąś akcję i zwraca this, zwraca obiekt, na którym możemy wywołać kolejną metodę.
Funkcja konstruktor – obiekty przy użyciu 'new’
Na razie mieliśmy do czynienia z „dziwnymi” obiektami, którym ręcznie przypisywaliśmy pewne wartości (takie jak imię, wiek). A potem odnosiliśmy się do tych wartości poprzez słówko kluczowe 'this’ (jakby ktoś nam zabraniał w miejsce 'this.name’ tak samo 'z palca’ napisać 'John’).
Cóż, obiekty prawdziwie „błyszczą”, kiedy są dynamicznie tworzone i stanowią pewien schemat, służący nam do tworzenia różnych od siebie, ale korzystających z podobnej konwencji obiektów.
Funkcja konstruktor to taka funkcja, która pozwala nam tworzyć obiekty. Dobrym zwyczajem piszemy jej nazwę wielką literą. Wygląda ona tak:
<script>
function Person(name, age) {
this.name = name;
this.age = age;
}
</script>
Aby jej użyć musimy zastosować słówko kluczowe new, jej nazwę i przekazać odpowiednie argumenty:
<script>
function Person(name, age) {
this.name = name;
this.age = age;
}
let person1 = new Person("John", 30);
let person2 = new Person("Jane", 20);
console.log(person1.name, person1.age);
console.log(person2.name, person2.age);
//John 30
//Jane 20
</script>
Tak wygląda ten mechanizm. Wystarczy się go nauczyć i już mamy pewną fabrykę, produkującą obiekty według określonego wzorca.
Tutaj – podajemy imię i wiek, otrzymujemy obiekt posiadający takie właśnie pola.
Możemy też do tego rodzaju obiektu dodać jakąś metodę:
<script>
function Person(name, age) {
this.name = name;
this.age = age;
this.introduce = function() {
console.log(`My name is ${this.name}`);
console.log(`I am ${this.age} years old`);
};
}
let person1 = new Person("John", 30);
let person2 = new Person("Jane", 20);
person1.introduce();
//My name is John
//I am 30 years old
</script>
Nie wchodząc w szczegóły – możemy też metodę dodać za pomocą czegoś takiego, jak prototyp:
<script>
function Person(name, age) {
this.name = name;
this.age = age;
}
Person.prototype.introduce = function() {
console.log(`My name is ${this.name}`);
console.log(`I am ${this.age} years old`);
};
let person1 = new Person("John", 30);
let person2 = new Person("Jane", 20);
person1.introduce();
//My name is John
//I am 30 years old
</script>
Tworzenie obiektów – bez new, zwróć literał obiektu
Jeżeli funkcje konstruktora wydają nam się dziwne – mają prawo – mamy inny, nawet lepszy sposób na tworzenie obiektów. Sposób, w którym nie będziemy potrzebować żadnego słówka kluczowego new do ich tworzenia.
W oparciu o poprzedni przykład postarajmy się napisać funkcję create_person, która przyjmuje parametry name i age oraz zwraca literał obiektu.
Nie jest to tak trudne, jak się wydaje:
<script>
function create_person(name, age) {
return {
name: name,
age: age
}
}
</script>
Dzięki nowej składni ES6+ możemy nawet skrócić ten proces, jeżeli nasze argumenty i pola obiektu nazywają się tak samo:
<script>
function create_person(name, age) {
return {
name,
age,
}
}
</script>
Trzymając się jednak tej nieco bardziej czytelnej wersji, dopiszmy jeszcze metodę do naszego literału obiektu:
<script>
function create_person(name, age) {
return {
name: name,
age : age,
introduce() {
console.log(`My name is ${this.name}, I am ${this.age} years old`)
}
}
}
</script>
Spróbujmy teraz utworzyć sobie obiekt w ten sposób:
<script>
function create_person(name, age) {
return {
name: name,
age : age,
introduce() {
console.log(`My name is ${this.name}, I am ${this.age} years old`)
}
}
}
let person = create_person("John", 30);
person.introduce();
//My name is John, I am 30 years old
console.log(person.name, person.age);
//John 30
</script>
Słówko kluczowe new nie było potrzebne. Są to dwa sposoby na tworzenie obiektu według określonego schematu (tutaj schematem jest posiadanie pól imię i wiek oraz metody przedstaw się).
Który sposób bardziej wygląda nam na intuicyjny, a który jest „dziwny”? To już od nas zależy, co najmniej jeden ze sposobów (funkcja konstruktor, funkcja zwracająca literał obiektu) z pewnością wygląda nam dziwnie i musimy się z nimi oswoić, a potem – używać tego, który nam bardziej odpowiada
Obiekty to temat rzeka. W następnych lekcjach poznamy inne ich właściwości i zastosowania, mam też kilka pomysłów na zadania utrwalające ich znajomość.
Tym niemniej – nauczyliśmy się dzisiaj naprawdę sporo i jest to dobry moment, aby zakończyć i raz jeszcze przeanalizować to, co już wiemy.
Do następnego razu!