13 października 2022 10 min. czytania

Programowanie obiektowe w języku JavaScript

Programowanie obiektowe jest podstawą wielu języków programowania. Dlatego zapoznamy się z tym paradygmatem, umieścimy go w kontekście i wykorzystamy w praktyce.

Zaktualizowano: 13 października 2022
Cztery filary betonowego budynku pod błękitnym niebem
Zdjęcie autorstwa Mayer Tawfik

Ostatnio pisałem o konkretnych technologiach: React, Gatsby czy Netlify CMS. W pierwszym wpisie, obiecałem jednak bardziej uniwersalną wiedzę. Dziesięć postów później, czas wypełnić obietnicę. Ten wpis będzie bardziej teoretyczny. Ale nie chcę żeby stał się zbyt abstrakcyjny, dlatego napiszę o OOP (Object Oriented Programming) w kontekście języka JavaScript.

OOP jest jednym z paradygmatów programowania

Zanim nawet dotkniemy definicję OOP, potrzebujemy krótkiej lekcji historii. Da nam ona kontekst, szerszy obraz, a co za tym idzie lepsze zrozumienie.

Każdy kod sprowadza się do zer i jedynek. To jest to, co rozumieją komputery. Najniższym paradygmatem programowania jest kod maszynowy, który reprezentuje instrukcje bezpośrednio, jako sekwencje cyfr. Jest łatwy do zrozumienia dla maszyn, ale trudny dla ludzi.

W latach 60. XX wieku, nastąpił rozwój języków assembly. Jest to mały krok naprzód w stosunku do kodu maszynowego. Pisząc w assemblerze mamy do dyspozycji operatory takie jak: READ, WRITE, GET i PUT. Możemy także używać symbolicznych etykiet dla adresów pamięci. Sam nawet pisałem coś tam w assemblerze na uczelni. Ten kod wyglądał mniej więcej tak:

NASM
1; hello-DOS.asm - single-segment, 16-bit "hello world" program
2;
3; assemble with "nasm -f bin -o hi.com hello-DOS.asm"
4
5 org 0x100 ; .com files always start 256 bytes into the segment
6
7 ; int 21h is going to want...
8
9 mov dx, msg ; the address of or message in dx
10 mov ah, 9 ; ah=9 - "print string" sub-function
11 int 0x21 ; call dos services
12
13 mov ah, 0x4c ; "terminate program" sub-function
14 int 0x21 ; call dos services
15
16 msg db 'Hello, World!', 0x0d, 0x0a, '$' ; $-terminated message

Pomimo, że trudno go zrozumieć i nie jest tak wyrafinowany jak współczesne języki programowania, ludzie nadal używają assemblera w rzeczach takich jak systemy wbudowane.

C, COBOL czy BASIC to przykłady języków trzeciej generacji. Wszystkie te języki podążają za proceduralnym paradygmatem programowania - komputer otrzymuje procedury, za którymi musi podążać, aby rozwiązać konkretny problem. Łatwiej pisać taki kod, ale nadal nie ma w nim struktury czy organizacji. Mamy po prostu instrukcje typu krok po kroku.

Z czasem ludzie zaczęli się zgadzać co do dobrych praktyk i języki zorientowane obiektowo, takie jak C# czy Python, zostały wynalezione. Od tej pory programiści mogli modelować rzeczy z rzeczywistości przy pomocy obiektów. Obiekt to podstawowa, fundamentalna jednostka budulcowa wszystkich tych języków.

Dodatkowo, programowanie funkcjonalne zyskuje popularność. Współpracuje ono dobrze z obliczeniami równoległymi i rozproszonymi - gorącymi tematami aktualnie. Funkcje są podstawową jednostką w tym paradygmacie. Zachowują się one jak matematyczne funkcje. Większość języków programowania wspiera więcej niż jeden paradygmat. JavaScript jest jednym z nich.

FP i OOP

We wszystkich programach istnieją dwa główne komponenty:

  • Dane
  • Zachowania

Programowanie zorientowane obiektowo mówi, że umieszczanie danych i zachowań w jednym miejscu - obiekcie - upraszcza zrozumienie działania programu. Można to porównać to zapakowania wszystkiego w szczelnie zamkniętym pudełku.

Programowanie funkcjonalne mówi, że dane i zachowania to dwie odrębne rzeczy i powinny być trzymane oddzielnie dla przejrzystości programu. Można je porównać do rury i serii zaworów - dane przepływają od jednego końca rury do drugiego, zmieniając kształt po drodze.

To nie są zawody pod tytułem: "programowanie funkcjonalne kontra programowanie zorientowane obiektowo". Używałem OOP pisząc w Node i FP w React. Nie trzymaj się kurczowo jednego paradygmatu. Oba mają swoje zastosowania i mogą być komplementarne.

Dlaczego używać OOP?

Tak jak wspomniałem chcemy, aby nasz kod był łatwy do zrozumienia. Częściej czytamy kod niż go piszemy. Współczesne oprogramowanie może być bardzo złożone. Windows 10 składa się z ok. 50 milionów linii kodu, a Facebook z jeszcze większej ich liczby. Przeciętna aplikacja może zawierać ich tysiące. Aby zorganizować jakoś ten złożony kod, możemy użyć naszego paradygmatu. Wykorzystując OOP chcemy, aby nasz kod był:

  • Jasny
  • Łatwy do rozbudowy
  • Łatwy w zarządzaniu
  • Pamięciowo wydajny
  • DRY (Don't Repeat Yourself)

OOP w języku JavaScript

Przeszliśmy przez krótką historię paradygmatów programowania wymieniając różne języki. Teraz skupimy się na jednym - języku JavaScript - i napiszemy trochę kodu, aby zaprezentować je w praktyce. Zaczniemy od strukturalnego kodu i stopniowo przejdziemy do kodu zorientowanego na obiekty. JavaScript ewoluuje i nie zawsze mieliśmy składnie do jasnego OOP.


Załóżmy, że mamy dwa komputery: stacjonarny i laptop. Chcemy modelować je w kodzie. Pierwszym, naiwnym podejściem będzie podejście proceduralne, które może wyglądać tak:

Kod proceduralnyJAVASCRIPT
1const desktop = {
2 name: 'Mac Studio',
3 gpu: 'AMD Radeon Pro W6800X',
4 info() {
5 console.log(`Nazwa komputera: ${this.name}`)
6 }
7}
8
9const laptop = {
10 name: 'Macbook Air',
11 display: '13.6"',
12 info() {
13 console.log(`Nazwa komputera: ${this.name}`)
14 }
15}

Możemy tworzyć odrębne obiekty dla każdej maszyny. Ale wyobraź sobie, że chcemy teraz dziesięć kolejnych. Musielibyśmy ręcznie stworzyć obiekt dla każdego komputera, albo kopiować wiele linii kodu. Stale byśmy się powtarzali.

Funkcje fabryczne

Aby ograniczyć powtarzanie, możemy napisać funkcję, która stworzy obiekty dla nas - funkcję fabryczną.

Funkcja fabrycznaJAVASCRIPT
1function createComputer(name) {
2 return {
3 name,
4 info() {
5 console.log(`Nazwa komputera: ${this.name}`)
6 }
7 }
8}
9
10const desktop = createComputer('Mac Studio')
11const laptop = createComputer('Macbook Air')

Możemy tę funkcję wywołać wiele razy, aby stworzyć wiele obiektów. Nie powtarzamy się już tak bardzo. Ale istnieje inny problem. Spójrz na funkcję info(). Pozostaje ona taka sama dla każdego komputera, a jednak kopiujemy ją za każdym razem. Nie jest to specjalnie oszczędne pod kątem pamięci.

Object.create()

Jakiś czas temu, programiści dodali metodę create() do wbudowanego obiektu Object. Wykorzystuje ona istniejący obiekt jako prototyp, dla nowo stworzonego obiektu. W ten sposób możemy zdefiniować funkcję raz i odziedziczyć ją wykorzystując dziedziczenie prototypowe.

Object.create()JAVASCRIPT
1const computerFunctions = {
2 info() {
3 console.log(`Nazwa komputera: ${this.name}`)
4 }
5}
6
7function createComputer(name) {
8 let newComputer = Object.create(computerFunctions)
9 newComputer.name = name
10 return newComputer
11}
12
13const desktop = createComputer('Mac Studio')
14const laptop = createComputer('Macbook Air')

Język JavaScript nie posiada klasycznego dziedziczenia znanego z języków takich jak Java czy C++. Posiada on tylko jedną konstrukcję - obiekt. Ale to nie jest bug - to jest feature! Każdy obiekt ma link do innego, który nazwany jest jego prototypem. Te powiązane obiekty tworzą łańcuch prototypowy.

Moglibyśmy się tu zatrzymać. Nie powtarzamy się i nasz kod nie zajmuje zbędnego miejsca. Ale możemy także ulepszyć inne części paradygmatu OOP. Wcześniej wspominałem o tych zamkniętych pudełkach. Nie mamy tu tego i powyższy fragment kodu jest trochę trudny do zrozumienia. Użyjmy naszej wiedzy na temat prototypów i sprawdźmy czy uda nam się coś ulepszyć.

Funkcje konstruktora

Każda funkcja wywołana używając słowa new to funkcja konstruktora. Wbudowane obiekty takie jak Array czy Function są właśnie funkcjami konstrukcyjnymi. W języku JavaScript wszystko posiada funkcję konstruktora (z wyjątkiem null i undefined). To słowo kluczowe new robi klika rzeczy za kulisami:

  • Tworzy nowy obiekt.
  • Zwraca ten obiekt.
  • Wiąże this do zwróconego obiektu.
  • Wiąże prototyp obiektu do funkcji konstruktora.
Funkcja konstruktoraJAVASCRIPT
1function Computer(name) {
2 this.name = name
3}
4
5Computer.prototype.info = function () {
6 console.log(`Nazwa komputera: ${this.name}`)
7}
8
9const desktop = new Computer('Mac Studio')
10const laptop = new Computer('Macbook Air')

Możemy wykorzystać prototyp do zdefiniowania współdzielonej funkcjonalności raz i będzie ona dostępna dla każdego obiektu stworzonego przy pomocy funkcji konstrukcyjnej.

Ale uważaj - to nie zadziała z funkcjami strzałkowymi. Funkcje strzałkowe używają zasięgu leksykalnego - definiują this na podstawie swojego miejsca w kodzie. Jeżeli zdefiniowalibyśmy funkcję info() jako strzałkową w naszym fragmencie, this wskazywałoby na obiekt window.

Programiści JavaScript pisali w ten sposób przez długi czas. Prawdopodobnie znalazłbyś podobne fragmenty kody w starszych repozytoriach. Ale wraz z ES6 dostaliśmy nowy sposób, aby pisać kod zorientowany obiektowo.

Klasy

Nowa składnia została wprowadzona (wraz z wieloma innymi funkcjonalnościami) w specyfikacji ECMAScript 6. Poniższy fragment może wyglądać znajomo dla programistów, którzy używali wcześniej C++ albo inny, obiektowy język.

KlasyJAVASCRIPT
1class Computer {
2 constructor(name) {
3 this.name = name
4 }
5 info() {
6 console.log(`Nazwa komputera: ${this.name}`)
7 }
8}
9
10const desktop = new Computer('Mac Studio')
11const laptop = new Computer('Macbook Air')

Chyba największą zaletą jest to, że wszystko jest umieszczone w jednym “pudełku”. Wszystkie właściwości i metody są pomiędzy klamrami. Nie musimy używać prototypów bezpośrednio.

Ale pamiętaj - pomimo tego, że istnieje słowo class, to jest to tak zwany lukier składniowy. Pod spodem, cały czas używamy dziedziczenia prototypowego. W innych językach klasy są realną rzeczą. W przeciwieństwie do języka JavaScript, gdzie istnieją tylko obiekty.

Możesz argumentować, że używanie słowa class wraz z dziedziczeniem prototypowym jest zwodnicze. I trochę jest. Ale w mojej opinii, korzyści wynikające z czytelności przeważają nad tą wadą. Myślę, że ten kod jest łatwy do zrozumienia i wygląda jak OOP. Szczególnie, gdy dodamy dziedziczenie.

Klasy i dziedziczenieJAVASCRIPT
1class Computer {
2 constructor(name) {
3 this.name = name
4 }
5 info() {
6 console.log(`Nazwa komputera: ${this.name}`)
7 }
8}
9
10class Desktop extends Computer {
11 constructor(name, gpu) {
12 super(name)
13 this.gpu = gpu
14 }
15}
16
17class Laptop extends Computer {
18 constructor(name, display) {
19 super(name)
20 this.display = display
21 }
22}
23
24const desktop = new Desktop('Mac Studio', 'AMD Radeon Pro W6800X')
25const laptop = new Laptop('Macbook Air', '13.6"')

Klasy Desktop i Laptop mają łańcuch prototypowy z klasą Computer. Podklasy dziedziczą z klasy bazowej (zwanej także super klasą). Aby odziedziczyć właściwości, musimy wywołać metodę super() wewnątrz konstruktora. Poniżej tej metody możemy dodać właściwości specyficzne dla podklasy. Ten typ dziedziczenia nie kopiuje wszystkiego z klasy bazowej jak w klasycznych językach obiektowych. Zamiast tego, poszerzamy łańcuch prototypowy wykorzystując extends. Ma to swoje zalety pod kątem optymalizacji pamięci.

Cztery filary OOP

Mówienie o filarach pod koniec wpisu może wydawać się dziwne. Ale są one dobrym podsumowaniem i my już je poznaliśmy!

Enkapsulacja

Spójrz na nasze klasy - wszystko jest wewnątrz obiektu owinięte przez klamry. Nasz kod składa się z zamkniętych jednostek. Poddaliśmy nasz kod enkapsulacji. Taki kod jest łatwy do zrozumienia.

Abstrakcja

Nasze przykłady nie są skomplikowane, aby uprościć wywód. Ale wyobraź sobie, że metoda info() jest złożona. Nie musisz wiedzieć jak działa, aby jej użyć. Złożoność jest wyabstrahowana przed tobą. Abstrakcja oznacza, że ukrywamy złożoność przed użytkownikiem. Taki kod jest łatwy w użyciu.

Dziedziczenie

Nasze klasy dziedziczą część funkcjonalności od siebie. Unikamy przepisywania i powtarzania siebie. Oszczędzamy także miejsce, definiując wspólne metody.

Polimorfizm

Dobra, skłamałem. Nie poznaliśmy jeszcze jednego z filarów. Ale odkupuję swoje winy w tym momencie. Słowo “polimorfizm” pochodzi od greckiego słowa “polymorphos”, które dosłownie oznacza “wiele postaci”. Etymologia dobrze opisuje to co chcemy osiągnąć polimorfizmem w OOP. Nie wiem czy istnieje jedna, konkretna definicja polimorfizmu. Nie chcę kłócić się z ludźmi mądrzejszymi ode mnie, dlatego nie twierdzę, że przytaczam precyzyjną definicję. Ale pomysł jest taki, że wywołujemy tę samą metodę na różnych obiektach i każdy z nich reaguje inaczej. JavaScript jest typowany dynamicznie i ogranicza polimorfizm. Ale możemy napisać coś takiego:

Klasy i polimorfizmJAVASCRIPT
1class Computer {
2 constructor(name) {
3 this.name = name
4 }
5 info() {
6 console.log(`Nazwa komputera: ${this.name}`)
7 }
8}
9
10class Desktop extends Computer {
11 constructor(name, gpu) {
12 super(name)
13 this.gpu = gpu
14 }
15 info() {
16 console.log(`Nazwa komputera: ${this.name}\nGPU: ${this.gpu}`)
17 }
18}
19
20class Laptop extends Computer {
21 constructor(name, display) {
22 super(name)
23 this.display = display
24 }
25 info() {
26 console.log(`Nazwa komputera: ${this.name}\nWyświetlacz: ${this.display}`)
27 }
28}
29
30const desktop = new Desktop('Mac Studio', 'AMD Radeon Pro W6800X')
31const laptop = new Laptop('Macbook Air', '13.6"')
32
33desktop.info()
34//Nazwa komputera: Mac Studio
35//GPU: AMD Readeon Pro W6800X
36laptop.info()
37//Nazwa komputera: Macbook Air
38//Wyświetlacz: 13.6"

Nasza metoda info() jest polimorficzna. W zależności od obiektu, wyświetla informacje o różnych peryferiach komputerowych.

Podsumowanie

Odkryliśmy zasady, koncepty i cele programowania zorientowanego obiektowo. Dodatkowo, wiemy teraz jak je zastosować w przyszłych projektach JavaScript. Mam nadzieję, że ten post był pomocny i że udało ci się dotrwać do końca. A jeżeli nie masz dość, sprawdź także poniższe linki:

Newsletter, który rozpala ciekawość💡

Subskrybuj mój newsletter, aby otrzymywać comiesięczną dawkę:

  • Nowości, przykładów, inspiracji ze świata front-end, programowania i designu
  • Teorii naukowych i sceptycyzmu
  • Moich ulubionych źródeł, idei, narzędzi i innych interesujących linków
Nie jestem nigeryjskim księciem, aby oferować ci okazje. Nie wysyłam spamu. Anuluj kiedy chcesz.

Pozostań ciekawy. Przeczytaj więcej

Połowa winyla na białym tle1 września 20227 min. czytania

Dostępne animacje w React

Czyli jak nie kręcić swoimi użytkownikami (jak winylem). Niektóre animacje mogą powodować problemy u użytkoników. Zadbamy o nich i sprawimy, że nieistotne animacje będą opcjonalne.

Czytaj wpis
Zdjęcie zmiennych CSS w edytorze Visual Studio Code14 września 20227 min. czytania

Konwertowanie tokenów projektowych na zmienne CSS z Node.js

Konwertowanie tokenów projektowych jest procesem podatnym na błędy - przekonałem się o tym na własnej skórze. Dlatego stworzyłem prosty skrypt dla środowiska Node.js, który pomoże mi z tym zadaniem.

Czytaj wpis
Pięć metalowych kół zębatych na czarnym tle23 września 202210 min. czytania

Gatsby z Netlify CMS

W tym wpisie przyjrzymy się bliżej Netlify CMS. Jest to przykład CMSa nowego typu, który jest oparty o Git. Zintegrujemy go z przykładowym projektem Gatsby.

Czytaj wpis