Funkcje konstruktory vs klasy w JavaScript

Wraz z wejściem w życie ECMAScript 6 otrzymaliśmy nowy element – klasy (class). Nie są to jednak klasy w rozumieniu innych języków obiektowych jak PHP czy Java. Tutaj nadal panują pewne charakterystyczne dla JS kwestie dziedziczenia prototypowego, a klasy tak na prawdę są pewnego rodzaju „uporządkowaniem” kodu, który często gościł w bardziej rozbudowanych skryptach. W dzisiejszym wpisie postaram się nakreślić czym są i jak działają „dotychczasowe” funkcje konstruktory oraz „nowoczesne” klasy.

Na wstępie należy wyraźnie zaznaczyć, że w JavaScript nie występują klasy w takim samym rozumieniu jak w PHP czy Javie. Owszem, jest jawny „konstruktor” czy możliwość „rozszerzania” klasy z użyciem słowa „extends”. Aby jednak zrozumieć czym na prawdę są „klasy” w JS trzeba wcześniej omówić sposób, jaki był używany do czasu pojawienia się ES6. Na końcu artykułu zobaczymy, że w zasadzie klasy to nie jakaś „super extra nowość”, a jedynie pewnego rodzaju „opakowanie” na coś, co do tej pory wymagało tworzenia niezbyt eleganckiego i czytelnego kodu.

Tworzenie funkcji konstruktora metodą „tradycyjną”

W języku JavaScript można tworzyć własne funkcje konstruktory, które następnie wywołuje się przy użyciu słowa new. Technika ta była stosowana zanim udostępnione zostały klasy (class) w standardzie ES6. Z konstruktorami wiązało się jednak kilka problemów. Na początek zobaczmy jak stworzyć funkcję, pełniącą rolę konstruktora obiektu:

const Person = function (firstName, lastName) {
    this.first = firstName;
    this.last = lastName;
    this welcome = function () {
        return `Welcome ${this.first} ${this.last}`;
    }
}

Następnie, aby wykorzystać nasz konstruktor musimy jawnie stworzyć nowy obiekt Person:

const student = new Person('Tomek','Sochacki');

student.first;     //'Tomek'
student.last;      //'Sochacki'
student.welcome(); //'Welcome Tomek Sochacki'

Teoretycznie wszystko działa tak jak tego oczekiwaliśmy. Zobaczmy jednak co tak na prawdę dzieje się w momencie wywołania „new Person()„. Otóż w pierwszej kolejności tworzony jest nowy obiekt, który zostaje zapisany w this. Właściwości zostają dodane do tego właśnie obiektu. Ostatnim krokiem jest niejawne zwrócenie obiektu this, chyba, że w sposób jawny konstruktor zwraca inny obiekt (ale o tym za chwilę).

Można więc powiedzieć, że „w tle” dzieją się następujące rzeczy:

const Person = function (firstName, lastName) {
    // this = {}
    this.first = firstName;
    this.last = lastName;
    this welcome = function () {
        return `Welcome ${this.first} ${this.last}`;
    }
    //return this;
}

Obiekt this nie jest do końca pustym obiektem, gdyż dziedziczy on prototypowo po Object.prototype co jest w większości przypadków zjawiskiem porządanym.

W dobie powszechnego stosowania ECMAScript 6 wiele osób z wielką fascynacją stosuje tzw. funkcje strzałkowe (arrow function). Pytanie jednak czy i w tym wypadku jest to możliwe? Spójrzmy na poniższy przykład, w którym ponownie stworzymy funkcję konstruktora Person, ale tym razem przy użyciu składni arrow function:

const Person= (firstName, lastName) => {
    this.first = firstName;
    this.last = lastName;
    this.welcome = function () {
        return `Welcome ${this.first} ${this.last}`;
    }
}
const student = new Person('Tomek','Sochacki');
//TypeError: Person is not a constructor
const worker = Person('Adam','Kowalski');
worker; //undefined

Tym razem próba utworzenia obiektu Person kończy się niepowodzeniem. Problem ten związany jest z charakterem arrow function, które mają bardzo precyzyjnie określony wskaźnik this. O funkcjach strzałkowych pisałem już na blogu więc nie będę tutaj powielał informacji, na tym etapie proponuję abyś przyjął za „pewnik”, że funkcji konstruktorów nie należy tworzyć z użyciem składni arrow function.

Co więcej, zauważ, że możliwe jest przypisanie do zmiennej worker wartości zwracanej przez funkcję Person. Problem jednak w tym, że w naszym przykładzie funkcja Person zwraca niejawnie return undefined„, więc de facto nic taką deklaracją nie zyskujemy.

Jawne zwrócenie wartości przez konstruktor

No dobrze, ale skoro Person deklarujemy z użyciem słowa function to przecież można w takiej funkcji konstruktorze użyć również słowa return. Nie jest to jednak takie proste jak mogłoby się wydawać.

const Person = function (firstName, lastName) {
    this.first = firstName;
    this.last = lastName;
    this.welcome = function () {
        return `Welcome ${this.first} ${this.last}`;
    }
    return 'UPS...';
}
const studentA = new Person('Tomek','Sochacki');
typeof studentA; //'object'
studentA.welcome(); //'Welcome Tomek Sochacki'
const studentB = Person('Tomek','Sochacki');
typeof studentB; //'string'
studentB; //'UPS...'
studentB.welcome(); //TypeError: studentB.welcome is not a function

Widzimy więc dwa skrajnie różne zachowania się konstruktora w momencie użycia słowa new i bez niego. A teraz spróbujmy jawnie zwrócić obiekt, posiadający metodę welcome, która „nadpisze” wcześniejszą metodę this.welcome:

const Person = function (firstName, lastName) {
    this.first = firstName;
    this.last = lastName;
    this.welcome = function () {
        return `Welcome ${this.first} ${this.last}`;
    }
    return {welcome(){return 'Invalid welcome...'}};
}

const a = new Person('x','y');
a.welcome(); //"Invalid welcome..."

const b = Person('x','y'); //BEZ UŻYCIA "NEW"
b.welcome(); //"Invalid welcome..."

typeof a; //'object'
typeof b; //'object'

W tym wypadku obie zmienne (a i b) zachowują się de facto tak samo. Jest to kolejna pułapka związana z używaniem funkcji w roli konstruktorów obiektów. Nie należy jednak mylić tego ze wzorcem modułu, w którym stosujemy tzw. funkcję natychmiastową IIFE, jawnie zwracającą konkretnie zaprojektowany obiekt. Nie będę tutaj omawiał tego wzorca aby nie wprowadzić niepotrzebnego zamieszania.

Widać zatem, że działanie funkcji konstruktorów jest zależne od wielu czynników i należy dokładnie przestrzegać nie tylko zasad ich wywoływania („new”) ale również prawidłowego tworzenia „ciała” konstruktora. Problem z brakiem wywołania słowem „new” można co prawda rozwiązać poprzez wewnętrzną weryfikację, czy obiekt stanowi instancję konstruktora ale nie do końca rozwiązuje to problem.

const Person = function (firstName, lastName) {
    if (!(this instanceof Person)) {
    	return new Person(firstName, lastName);
    }
    this.first = firstName;
    this.last = lastName;
    this.welcome = function () {
        return `Welcome ${this.first} ${this.last}`;
    }
}

const a = new Person('Tomek','Sochacki');
const b = Person('Tomek','Sochacki')

typeof a; //"object"
typeof b; //"object"

Tym razem jesteśmy teoretycznie zabezpieczeni przed ryzykiem wywołania konstruktora z pominięciem słowa new. Dlaczego jednak „teoretycznie”? Otóż dlatego, że mamy tutaj dwa kolejne problemy. Po pierwsze tworząc każdy konstruktor musimy powielać analogiczną instrukcję warunkową IF, a po drugie w instrukcji tej musimy jawnie podać nazwę tworzonej funkcji konstruktora. A co jeśli z jakiś powodów będziemy chcieli ją zmienić? Zmiany musimy wykonać wtedy w co najmniej dwóch miejscach. Oczywiście zmiana nazwy funkcji nie jest dobrą praktyką, ale czasami, np. przy refaktoryzacji bywa to przydatne.

Nie należy tego problemu rozwiązywać wykorzystując właściwość arguments.callee ponieważ nie jest ona obsługiwana w trybie ścisłym.

Pozostaje jeszcze jeden problem, otóż inny programista widząc w kodzie wywołanie „let student = Person('xxx','yyy')” nie do końca może być pewien czy ma do czynienia z konstruktorem czy ze „zwykłą” funkcją. Przyjęło się, że nazwy funkcji konstruktorów piszemy z dużej litery w notacji PascalCase (a nie camelCase jak przy tradycyjnych funkcjach), ale należy pamiętać, że jest to tylko konwencja umowna w żaden sposób nie weryfikowana przez silnik JavaScript.

Metody dodawane do funkcji konstruktora czy do prototypu?

Ok, no to mamy już mętlik w głowie związany z problematycznym wywoływaniem funkcji konstruktora podczas tworzenia obiektów. Czas więc zając się kolejną kwestią, a mianowicie dodawaniem metod, które będzie mógł wywoływać obiekt stworzony funkcją konstruktorem.

Mamy tutaj dwie możliwości. Sposób pierwszy to dodanie metody bezpośrednio w funkcji konstruktora z użyciem wskaźnika this:

const Person = function (firstName, lastName) {
    this.first = firstName;
    this.last = lastName;
    this.welcome = function () {
        return `Welcome ${this.first} ${this.last}`;
    }
}

const worker = new Person('Tomek','Sochacki');
worker.welcome(); //'Welcome Tomek Sochacki'

Wadą tej metody jest fakt, że za każdym razem gdy tworzymy obiekt Person (z użyciem składni new Person()) w pamięci przechowywana jest nowa wersja funkcji welcome(). Jest to nieefektywne, gdyż ciało tej funkcji nie zmienia się i jest identyczne dla każdego obiektu. Problem ten można rozwiązać poprzez dodanie metody welcome do prototypu funkcji Person:

const Person = function (firstName, lastName) {
    this.first = firstName;
    this.last = lastName;
}

Person.prototype.welcome = function () {

   return `Welcome ${this.first} ${this.last}`;
}
const  worker = new Person('Tomek','Sochacki');
worker.welcome(); //'Welcome Tomek Sochacki'

Tym razem przy tworzeniu kolejnych obiektów Person nie są tworzone nowe wersje metody welcome, a każde odwołanie do niej powoduje odwołanie do Person.prototype.welcome, czyli identycznej funkcji dla każdej instancji obiektu.

Problem jednak w tym, że w tym wypadku najpierw musimy stworzyć konstruktor obiektu, a dopiero później kolejno dodawać metody do jego prototypu. Jest to mało eleganckie rozwiązanie i trudne w utrzymaniu. Zobaczmy więc co oferuje nowa składnia class, wprowadzona w standardzie ECMAScript 6…

Czym są klasy w JavaScript

Przede wszystkim należy jasno powiedzieć czym klasy nie są – otóż nie są „tradycyjnymi” klasami w rozumieniu takich języków jak PHP, Java, C++ itp. W przypadku JavaScript cały czas mamy do czynienia z tzw. dziedziczeniem prototypowym dlatego nawet używając class warto dobrze poznać zasady działania prototypów w JavaScript.

Gdy ktoś pyta mnie czym są klasy w JS to najczęściej odpowiadam, że jest to bardziej elegancka, bezpieczniejsza i łatwiejsza w utrzymaniu forma tworzenia konstruktorów obiektów z metodami dodanymi do jego prototypu. Zobaczmy jak można nasz konstruktor Person „przepisać” na składnię wykorzystującą class:

class Person {

    constructor(firstName, lastName) {

        this.first = firstName;
        this.last = lastName;
    }

    welcome() {
        return `Welcome ${this.first} ${this.last}`;
    }
}


const worker = new Person('Tomek','Sochacki');
worker.welcome(); //'Welcome Tomek Sochacki'

W tym prostym przykładzie wyraźnie widać, dlaczego napisałem, że jest to bardziej elegancka forma. Pierwszym „elementem” jaki deklarujemy jest właśnie konstruktor (constructor), który jest wywoływany w momencie tworzenia obiektu z użyciem słowa new Person().

Dalej deklarujemy metodę welcome(), jednakże w przeciwieństwie do „wewnętrznej” deklaracji metod w funkcjach konstruktorach, w tym wypadku metoda ta „w tle” dodawana jest do Person.prototype.

Zalety klas w porównaniu do funkcji konstruktorów

Spróbuj przypomnieć sobie problemy związane z tworzeniem funkcji konstruktorów jakie omawiałem na początku artykułu. Teraz pokażę, dlaczego stosowanie class jest bezpieczniejszym rozwiązaniem i łatwiejszym w utrzymaniu.

Problemy związane z brakiem słowa new

Funkcja konstruktora musi być wywołana z użyciem new Person() aby umożliwić stworzenie wewnętrznego obiektu this. Brak słowa new może rodzić problemy z niewłaściwym odniesieniem do wskaźnika this. Problem ten można częściowo rozwiązać poprzez sprawdzenie, czy obiekt jest instancją danego konstruktora ale jest to mało wygodne rozwiązanie i konieczne jest jego powtarzanie dla wszystkich funkcji konstruktorów.

A co na to powiedzą klasy ECMAScript 6? Otóż tutaj jesteśmy po bezpiecznej stronie, gdyż instancji obiektu Person nie da się stworzyć inaczej niż z użyciem słowa new:

class Person {

    constructor(firstName, lastName) {

        this.first = firstName;
        this.last = lastName;
    }

    welcome() {
        return `Welcome ${this.first} ${this.last}`;
    }
}


const worker = Person('Tomek','Sochacki');
//TypeError: Class constructor Person cannot be invoked without "new"

Jak widać nie ma możliwości stworzenia obiektu inaczej, niż z użyciem new. Zabezpiecza nas więc to przed celową lub przypadkową próbą jego pominięcia.

Wartości zwracane przez konstruktor

Jeśli tworzymy konstruktor metodą funkcji konstruktora (przed ES6) to mieliśmy możliwość jawnego zwrócenia określonego obiektu. Problem jednak stanowiło ponownie wywołanie funkcji konstruktora bez słowa new, gdyż w tym wypadku możliwe było zwrócenie wartości innej niż obiekt (np. string, number itp.) co nie było dobrym rozwiązaniem.

Tworząc klasy również możemy w konstruktorze zawrzeć instrukcję return, jednakże zostanie ona „spełniona” jedynie wtedy, gdy zwracaną wartością będzie obiekt, przy czym należy pamiętać, że w JS obiektami są również np. tablice.

class Person {

    constructor(firstName, lastName) {

        this.first = firstName;
        this.last = lastName;
        return {message: 'Hello'};
    }

    welcome() {
        return `Welcome ${this.first} ${this.last}`;
    }
}

const worker = new Person('Tomek','Sochacki');

worker.message;   //"Hello"
worker.welcome(); //"TypeError: worker.welcome is not a function"

Gdybyśmy jednak w instrukcji return próbowali przekazać wartość typu prostego to zostanie ona zignorowana:

class Person {

    constructor(firstName, lastName) {

        this.first = firstName;
        this.last = lastName;
        return 'Hello';
    }

    welcome() {
        return `Welcome ${this.first} ${this.last}`;
    }
}

const worker = new Person('Tomek','Sochacki');
worker.welcome(); //"Welcome Tomek Sochacki"

Osobiście uważam jednak, że próba zwracania w konstruktorze innego obiektu to proszenie się o kłopoty i odradzam taką praktykę, chyba, że robisz to w pełni świadomie i w konkretnym celu. Warto jednak takie rozwiązanie dokładnie opisać w komentarzach i dokumentacji projektu.

Metody dodawane automatycznie do prototypu

Omawialiśmy wcześniej zalety dodawania metod do prototypu zamiast do funkcji konstruktora z przypisaniem ich do wewnętrznego obiektu this. Stosując klasy JS dodawanie metod do prototypu odbywa się w pewnym sensie z automatu. Zobaczmy na poniższy przykład:

class Person {

    constructor(firstName, lastName) {

        this.first = firstName;
        this.last = lastName;
    }

    welcome() {
        return `Welcome ${this.first} ${this.last}`;
    }
}

const worker = new Person('Tomek','Sochacki');

worker;
//Person {first: "Tomek", last: "Sochacki"}

Person.prototype;
//Object {constructor: function, welcome: function}

Widać zatem wyraźnie rozdzielenie właściwości indywidualnych dla każdej instancji obiektu Person oraz metod wspólnych, zawartych w Person.prototype. Jednocześnie wszystko deklarujemy wewnątrz jednej składni class Person {...} co zapewnia lepszą organizację kodu i jego utrzymanie.

Podsumowanie

Celem tego artykułu nie było dokładne omówienie składni klas i ich możliwości, a jedynie chciałem zwrócić uwagę aby nie porównywać klas JavaScript w sposób bezpośredni do klas z innych języków obiektowych. Uważam zatem, że każdy programista JavaScript wcześniej czy później powinien zagłębić się w tematykę dziedziczenia prototypowego gdyż nie uciekniemy od tego zagadnienia.

Ponad to klasy, poza ładniejszą formą i gwarancją większego bezpieczeństwa, mają również kilka innych ciekawych elementów jak chociażby extends (rozszerzanie o podklasy), super (wykorzystywane w podklasach), static (metody „statyczne”), możliwość deklarowania akcesorów get/set. Zagadnienia te omówię jednak z oddzielnych wpisach obrazując je na konkretnych przykładach.

Czy możemy korzystać ze składni class w starszych środowiskach? Tak, wystarczy użyć transpilatora kodu, np. Babel. Nie oznacza to, że wcześniejsza forma z funkcją konstruktorem jest zła – wymaga ona jednak większej dyscypliny i uwagi podczas pisania konstruktora i jego późniejszego wykorzystania. Użycie klas wydaje mi się, że jest przyjemniejszą formą i bardziej czytelną.

  • Fakt istnienia prototypów i stworzenia do tego klas jako cukru składniowego nie sprawia, że klasy są jakies gorsze w porównaniu do tych z php czy javy, jeśli są to czy mógłbyś napisac dlaczego? Skąd wiesz, że inne języki też nie mają prototypów pod spodem? Z tego co się przyglądałem to w niektórych językach przewija się `__proto__`.

    Dodatkowo dziedziczenie tablic przy prototypach sprawia ponoć problemy, gdzie przy klasach przechodzi to normalnie.

    Tak przy okazji nie stosuj `let` tam gdzie zmiennej nie nadpisujesz, lepiej użyć const, szczególnine przy wywołaniu konstruktora.

    Dodatkowo nie ma CamelCase a PascalCase.

    • Nie twierdzę, że tylko JS ma prototypy, ani że klasy w innych językach są gorsze. Napisałem jedynie, aby nie porównywać bezpośrednio klas JS do np. klas PHP bo to dwa różne mechanizmy. w JS klasy są ja to określiłeś pewnym lukrem składniowym na to co de facto dzieje się pod spodem. W PHP natomiast działa to zupełnie inaczej, ponad to mamy tam np. interfejsy, klasy abstrakcyjne, cechy itp.
      Reasumując klasy i w JS i w PHP są dobre, ale trzeba mieć na uwadze co tak na prawdę robią i tu i tu 🙂

      „Dodatkowo dziedziczenie tablic przy prototypach sprawia ponoć problemy, gdzie przy klasach przechodzi to normalnie.” Nie bardzo rozumiem co masz tu na myśli? JS nie ma oddzielnego typu dla tablic, są to po prostu obiekty z tzw. konstruktorem Array.prototype ale szczerze mówiąc nie wiem o jakich problemach mówisz.

      Co do PascalCase to faktycznie, dzięki za info, a tak dla ciekawostki spotkałem się kiedyś z zapisem UpperCamelCase jako === PascalCase 🙂 Poprawię to zaraz w poście.

      Natomiast co do deklaracji let to nie do końca jest tak jak piszesz. Osobiście uważam, że const najlepiej używać tam, gdzie na prawdę masz stałą, czyli do typów prostych. Natomiast gdy w const dasz obiekt czy tablicę to de facto nie możesz tylko zmienić typu wartości, ale sama wartość może być modyfikowana. Na przykład dodaj sobie (w odniesieniu do mojego ostatniego listingu z tego posta):

      const worker = new Person(‚Tomek’,’Sochacki’);
      worker.hello = „hello”;

      worker.hello; //”hello”

      Czyli udało się zmodyfikować obiekt. Dlatego uważam, że stosowanie tu const sugeruje stałość, gdy tak do końca nie jest. Ale to już moja subiektywna ocena i chętnie poznam inne punkty widzenia na ten problem.
      Pozdrawiam

      • Dlaczego to dwa różne mechanizmy? To że w php interfejsy i klasy abstrakcyjne czy cechy to jeszcze o niczym nie świadczy, bo JS dopiero wprowadza część rzeczy, zresztą mamy też TypeScript.

        Pod tym względem, mechanika klas jest nieco inna niż prototypów i w tym przypadku nie jest cukrem składniowym:
        https://stackoverflow.com/questions/8859828/javascript-what-dangers-are-in-extending-array-prototype

        const stosujesz po to, aby tego w/w workera nigdy nie nadpisał na zasadzie: worker = ‚Dan’;
        Jeśli nie chcesz aby jakieś właściwości zostały zmieniane to stosujesz domknięcia, Symbole, lub private z TypeScript, również masz immutable.js.
        Symbole:
        const hello = Symbol(‚hello’);
        this[hello] = „Hello”;

        const stosuje się do tego aby zmiennej nikt nie nadpisał, takiego leta można kiedy się chce i wtedy wszystkie metody się wysypują, bo nie mogą być wykonane na nowej wartości.

      • Ciekawa sprawa z tym rozszerzaniem Array.prototype, nie wiedziałem o tym. Dzięki za informację i bardzo cenny komentarz (czyli jednak ktoś tam tego bloga czytuje… 🙂

        Szczerze mówiąc to osobiście omijam jak ognia składni for-in i wolę zamiast tego stosować albo metody Array.prototype jak map, forEach, every itp. albo „tradycyjne” for, dlatego nie spotkał mnie omawiany tu problem.

        Można by go rozwiązać poprzez zastosowanie zabezpieczenia w postaci metody array.hasOwnProperty(index) i z tego co sobie testowałem to wtedy ominie dodatkowe elementy dodane do prototypu, ale trzeba o tym zawsze pamiętać (choć i tak uważam to za dobrą praktykę, aby w for-in zawsze ładować hasOwnProperty, chyba, że celowo to pomijamy).

        Pisałem niedawno artykuł o iterowaniu po tablicach i w for-in wskazywałem na jeszcze jedną pułapkę, a mianowicie wartość iterowana (najczęściej let i albo let prop) zwracana jest jako string, a nie liczba. W moim poście jest z tego co pamiętam przykład jak może to prowadzić do pewnych problemów:)

        A wracając do const to po przemyśleniu przyznaję Ci rację. Skupiłem się w mojej logice głównie na tym, że const de facto nie jest STAŁĄ niemodyfikowalną, nieruszalną itp. itd., ale fakt, daje gwarancję, że nie zostanie nadpisana. Poprawię zapisy w listingach z tego posta na const.

        Jeszcze raz dzięki za bardzo cenną dyskusję i konstruktywne uwagi i zapraszam w przyszłości również na mojego bloga 🙂
        Pozdrawiam

      • Ostatnio właściwie używam dość mocno .map do transformacji tablic, .filter do filtrowania i .reduce do chociażby spłaszczania. forEach do wykonania czegoś na każdym elemencie.
        Warto się tym zainteresować bo można pisać bardzo czytelny kod, dodatkowo każda taka metoda powinna robić tylko jedną rzecz. 😀

        Co do for-in to myślę że był jakąś wczesną implementacją i lepiej używać for-of, jeśli dobrze pamiętam to on iteresuje po swoich propertiesach. Również na obiektach gdy operuje to ciekawą opcją jest Object.keys(obj).map(key => obj[key]);

        Od siebie polecam http://exploringjs.com/, ma wiele ciekawych rzeczy o JS o ile się nie znudzisz podczas przechodzenia przez znane tematy.

        Warto dbać aby polski webdev w końcu zaczął dorównywać zagranicznym. Jeszcze zaglądne na parę artykułów i dam feedback. A zaglądnąłem na niego bo nie pisał o tak banalnych rzeczach jak „top 5 wtyczek do x” czy chociażby „jak zrobić kotka w css”, tylko porusza ciekawe rzeczy o JS.
        Pozdrawiam.

      • Szczerze mówiąc to uruchamiając bloga miałem taki cichy plan „JavaScriptowej setki”, czyli min. 100 artków dotyczących stricte JS oraz regexp w JS (+ nieco o DOM, ale to jako poboczne wątki). Zobaczymy czy czas pozwoli dotrwać do celu 🙂

        Metody Array.prototype są wg mnie bardzo dobre, bo jasno wskazują cel iterowania po tablicy. Do Twojej listy warto dodać jeszcze np. every, some, includes, find, findIndex czy reduceRight (choć z tego rzadko korzystałem).

        Mam nawet w planie opisać te metody na konkretnych przykładach tylko muszę się jakoś bardziej systematycznie do tego bloga zabrać 🙂

      • Z includes również korzystam, bardzo przydatne, dzięki za hasła, będzie trzeba to zaimplementuje. 😀
        Warto pisać kiedy ma się wenę, wtedy IMHO lepiej to wszystko wygląda.

  • Jestem na etapie nauki OOP z tradycyjnymi konstruktorami, ale możliwość użycia klas w js bardzo mnie cieszy i ułatwia sprawę, jak twierdzisz. Świetny blog, dobrze, że tu trafiłem

    • Jeśli mogę coś doradzić to poznaj dobrze „tradycyjne konstruktory” jak to nazwałeś. Potem zaczniesz używać klasy i pewnie tak już zostanie 🙂

      A dlaczego te konstruktory? Z prostego powodu – w JS codziennie dochodzi w necie wiele nowych skryptów/bibliotek itp. ale wiele niestety jest wątpliwej jakości. ES6 mimo, że obecne już od pewnego czasu nadal nie jest jeszcze aż tak powszechnie stosowane. Warto więc znać starsze metody aby w razie czego móc sobie dostosować czy poprawić wadliwy skrypt innego autora lub po prostu aby móc go przeanalizować i stworzyć coś własnego.