Kilka postów temu pokazałem jak w TypeScripcie tworzyć własne typy przy użyciu interfejsów i enumeratorów. Teraz czas na to aby stworzyć własny typ przy użyciu klas. Jest to zdecydowanie jeden z najpopularniejszych sposobów na tworzenie typów w TS’ie i chyba jeden z najczęściej używanych mechanizmów w tym języku w ogóle.
Prawda jest taka, że tworzenie klas nie jest cechą unikatową dla TS, a częścią specyfikacji EcmaScript6. Jednak w połączeniu z opcjami, które daje nam TS, klasy stają się naprawdę potężnym narzędziem służącym do tworzenia obiektowego kodu.
Jest jedna rzecz, o której muszę napisać już na samym początku. Ta część ES6, która dodaje do JavaScriptu możliwość tworzenia klas, to tylko upiększenie składni. Nie zmienia się w żaden sposób działanie języka. W środku to wciąż kod obiektowy bazujący na prototypach. Jednak dzięki klasom można korzystać z bardziej przejrzystych (według mnie) konstrukcji.
Aby stworzyć nową klasę wystarczy użyć operatora class, podać nazwę tworzonej klasy (tradycyjnie z wielkiej litery) oraz otworzyć parę nawiasów ‚wąsatych’:
1 2 3 |
class Dog { } |
I to wystarczy. W ten sposób stworzona została nowa klasa w TypeScript 🙂 . Póki co nie ma ona jednak żadnej funkcjonalności. pierwszą rzeczą, którą trzeba by było dodać to konstruktor. Konstruktor to specjalna metoda, która używana jest do tworzenia instancji klasy.
1 2 3 |
class Dog { constructor(){} } |
Teraz mogę stworzyć obiekt, który bazuje na klasie dog:
1 2 3 4 5 |
class Dog { constructor(){} } var maja:Dog = new Dog(); |
jak widać zmienna maja zawiera teraz instancję nowej klasy. Nie mogłaby przetrzymywać innej informacji, ponieważ jest typu Dog.
Wygląda to całkiem nieźle, ale wciąż klasa ta nie ma w sobie żadnej konkretnej logiki. Przydałoby się jeszcze coś dodać. Pierwszą rzeczą, która przychodzi do głowy to pola w klasie, które można byłoby ustawić podczas tworzenia obiektu.
Pola można deklarować wewnątrz klasy, tak jak normalne zmienne. Każda instancja klasy będzie miała własną kopię takiego pola:
1 2 3 4 5 6 7 8 9 |
class Dog { name:string; state:string = 'happy'; constructor(){} } var maja:Dog = new Dog(); console.log(maja.state); |
Klasa otrzymała dwa pola typu string: name oraz state. Domyślnie pole name jest puste a state otrzymuje wartość happy.
W taki sam sposób mogę dodawać metody do klasy:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
class Dog { name:string; state:string = 'happy'; constructor(){} bark():string { return "Woof! Woof! I am "+this.state; } } var maja:Dog = new Dog(); console.log(maja.state); console.log(maja.bark()); |
Jak widać, przy okazji tworzenia metod nie potrzeba używać słówka function. Po nazwie metody wpisuję nawiasy, które mogą zawierać ewentualne argumenty. Następnie po dwukropku typ zwracanej wartości i blok funkcyjny. Aby dostać się do zawartość jakiegoś pola klasy, trzeba użyć słówka this, tak jak w normalnym JavaScript.
Teraz mogę udoskonalić mój konstruktor. Najlepiej byłoby, gdyby przyjmował jako argument wartość, którą mogę przypisać do pola klasy. Mogę zrobić to w taki sposób:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
class Dog { name:string; state:string = 'happy'; constructor(name:string){ this.name = name } bark():string { return "Woof! Woof! I am "+this.name+". And I am "+this.state; } } var maja:Dog = new Dog('maja'); console.log(maja.state); console.log(maja.bark()); |
Nic prostszego. W konstruktorze wpisuje argument oraz jego typ i przypisuję do pola name klasy.
Ale mam w zanadrzu jeden mały trick TypeScriptowy, który jeszcze bardziej usprawni ten kod. Jeżeli do argumentu w konstruktorze dopiszę słówko public, nie będę musiał nigdzie deklarować tego pola ani wykonywać żadnych przypisań. Zamiast tego TS sam doda do klasy pole o nazwie równej nazwie parametru oraz o wartości takiej jaka zostanie przekazana w konstruktorze:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
class Dog { state:string = 'happy'; constructor(public name:string){} bark():string { return "Woof! Woof! I am "+this.name+". And I am "+this.state; } } var maja:Dog = new Dog('maja'); console.log(maja.state); console.log(maja.bark()); |
Ten kod nie zgłosi żadnego błędu i wyloguje co trzeba. Pomimo tego, że nigdzie nie zadeklarowałem pola name i nie przypisałem do niego wartości. Prawda, że sprytne? 🙂
Dodawane w ten sposób do klasy pola i metody, będą kopiowane dla każdej jej instancji. Innymi słowy, każdy obiekt utworzony na bazie klasy, będzie miał swoją wersję pól i metod. Czasem jednak chcemy aby było inaczej, aby metoda lub (rzadziej) pole, było dzielone dla wszystkich instancji. W czystym JSie nie jest łatwo tego dokonać. Najłatwiejszy sposób na osiągnięcie tego to użycie zmiennych globalnych. Mało eleganckie rozwiązanie.
Na szczęście w ES6 (a co za tym idzie w TypeScript), mamy możliwość tworzenia statycznych elementów w klasach. Statyczne pole lub funkcja, będą „dzielone” przez wszystkie instancje. Do statycznego elementu trzeba odwoływać się używając nie słówka this a nazwy klasy. Oto przykład:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
class Dog { static id:number = 0; id:number; state:string = 'happy'; constructor(private name:string){ this.id = ++Dog.id; } bark():string { return "Woof! Woof! I am "+this.name+". And I am "+this.state+". My id is "+this.id; } } var maja:Dog = new Dog('maja'); console.log(maja.bark()); var milus:Dog = new Dog('milus'); console.log(milus.bark()); var dzeki:Dog = new Dog('dzeki'); console.log(dzeki.bark()); |
Do klasy Dog dodałem dwa pola typu liczbowego o nazwie id. Jedno jest statyczne (dzielone pomiędzy instancjami) a jedno „normalne” (unikatowe dla każdej instancji). Na początku wartość statycznego pola jest równa zero. Zwykłe id nie otrzymuje w wartości. Dopiero w konstruktorze przypisuje mu wartość aktualnego statycznego ID, które zwiększam najpierw o jeden. W ten sposób każdy nowy obiekt będzie miał pole id z wartością o jeden większą niż poprzedni. Wystarczy spojrzeć na to co logują przykłady.
W taki sam sposób mogę stworzyć statyczną metodę:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
class Dog { static id:number = 0; id:number; state:string = 'happy'; constructor(private name:string){ this.id = ++Dog.id; } static printBark(name:string,state:string, id: number):string { return "Woof! Woof! I am "+name+". And I am "+state+". My id is "+id; } bark():string { let bark:string = Dog.printBark(this.name,this.state,this.id) return bark; } } var maja:Dog = new Dog('maja'); console.log(maja.bark()); var milus:Dog = new Dog('milus'); console.log(milus.bark()); var dzeki:Dog = new Dog('dzeki'); console.log(dzeki.bark()); |
W tym przykładzie dodałem metodę printBark, która będzie ‚sklejać’ łańcuch znaków zwracany w metodzie bark. W jej wnętrzu nie mogę odnosić się do kontekstu klasy, ponieważ jest to metoda statyczna. Dlatego dane opisujące instancje będą przekazywane do niej w argumentach wewnątrz metody bark. Tak samo jak w przypadku pól statycznych, printBark wywołuję nie przez this a przez nazwę klasy czyli Dog.
I to tyle na dziś. Wiadomo już jak tworzyć nowe klasy przy użyciu konstruktorów i jak dodawać do nich metody i pola (zarówno zwyczajne jak i statyczne). W kolejnym poście pokaże jak działają gettery i settery oraz jak zmieniać dostępność pól i metod wewnątrz klasy.
Jeżeli chcesz być na bieżąco z postami na blogu zachęcam do polubienia mojej strony na facebooku. Zawsze zamieszczam tam informacje o wszystkich nowościach. Jest to też dobre miejsce na kontakt ze mną. Na wszystkie pytania zawsze odpowiem :).
Cały czas jestem pełen uznania 🙂 Dzięki!
Świetny wpis 🙂
Mam tylko jedną wątpliwość: czy w konstruktorze nie powinno się zawsze zaczynać od super(); ?
Na wypadek gdyby np. klasa dziedziczyła po innej klasie.