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!