Class - dziedziczenie

W poprzednim rozdziale poznaliśmy konstruktory i związane z nimi dziedziczenie oparte o prototypy.

Wykorzystanie funkcji jako konstruktorów to tylko jedna z możliwości tworzenia obiektów o podobnych funkcjonalnościach. Kolejną, którą poznamy będzie użycie składni klas.

Aby rozszerzyć jakąś klasę, a tym samym dziedziczyć po niej jej funkcjonalności, skorzystamy z instrukcji extends:


class Enemy {
    constructor(name, x, y) {
        this.name = name;
        this.x = x;
        this.y = y;
        console.log("Tworzę przeciwnika: " + this.name);
    }

    fly() {
        return this.name + " latam";
    }
}

class EnemyShoot extends Enemy {
    constructor(name, x, y) {
        super(name, x, y);
        this.type = "shooter";
    }

    shoot() {
        return this.name + " strzelam";
    }
}

const enemyN = new Enemy("Normal");
console.log(enemyN.fly()); //"Normal latam"
console.log(enemyN.shoot()); //błąd - nie ma takiej metody

const enemyS = new EnemyShoot("Shooter");
console.log(enemyS.fly()); //"Shooter latam"
console.log(enemyS.shoot()); //"Shooter strzelam"

Powyższa klasa rozszerza klasę Enemy pobierając z niej wszystkie funkcjonalności tamtej klasy, a dodatkowo tworzy nową metodę shoot().

Zauważ, że w konstruktorze pojawiła nam się instrukcja super() (linia 16), która służy do wywołania kodu rozszerzanej metody. W powyższym przykładzie będzie to kod konstruktora z klasy, którą właśnie rozszerzamy. Jeżeli wymagał on jakiś wartości, powinniśmy je przekazać w nawiasach super().


class Point {
    constructor(x, y) {
    }
}

class Dot extends Point {
    constructor(x, y, color) {
        super(x, y);
        this.color = color;
    }
}

W przypadku rozszerzania innych metod, także skorzystamy z instrukcji super, po której podajemy nazwę rozszerzanej metody:


class Enemy {
    constructor(name, x, y) {
        this.name = name;
        this.x = x;
        this.y = y;
        console.log("Tworzę przeciwnika: " + this.name);
    }

    fly() {
        return this.name + " latam";
    }
}

class EnemyShoot extends Enemy {
    constructor(name, x, y) {
        super(name, x, y);
        this.type = "shooter";
    }

    shoot() {
        return this.name + " strzelam";
    }

    fly() {
        const text = super.fly();
        return text + " i czasami strzelam!!!";
    }
}

const enemyN = new Enemy("Normal");
enemyN.fly(); //"Normal latam"

const enemyS = new EnemyShoot("Shooter");
console.log(enemyS.fly()); //"Shooter latam i czasami strzelam!!!""

I tutaj ponownie - gdyby funkcja fly() wymagała parametrów, przydało by się je podać, inaczej rozszerzana funkcja odpalona była by z wartościami undefined.


class Enemy {
    flyTo(x, y) {
        return `Lecę do pozycji ${x} : ${y}`
    }
}

class EnemyShoot extends Enemy {
    flyTo(x, y) {
        const text = super.flyTo(); //wywołaliśmy tamtą funkcję bez wartości
        return text + ' i strzelam!';
    }
}

const enemyN = new Enemy("Normal");
enemyN.flyTo(100, 200); //"Lecę do pozycji 100 : 200"

const enemyS = new EnemyShoot("Shooter");
console.log(enemyS.flyTo(100, 200)); //"Lecę do pozycji undefined : undefined i strzelam!"

W przypadku konstruktora metoda super() powinna być użyta przed pierwszym odwołaniem się do this. W przeciwnym razie dostaniemy błąd:


    class Shape {
        constructor(x, y, side) { }
    }

    class Square extends Shape {
        constructor(x, y, side) {
            this.type = "square";
            super(x, y, side); //błąd : Must call super constructor in derived class before accessing 'this' or returning from derived constructor
        }
    }

    const n = new Square(10, 10, 30);
    

Jeżeli chcemy taką metodę przesłonić (czyli by nic nie dziedziczyła z klasy rozszerzanej) to piszemy ją nie używając w jej wnętrzu super:


class Enemy {
    flyTo(x, y) {
        return `Lecę do pozycji ${x} : ${y}`
    }
}

class EnemyShoot extends Enemy {
    flyTo(x, y) {
        //nie używamy super.flyTo(x, y);
        return "Latam jak szalony";
    }
}

A jeżeli chcemy by dana metoda (w tym konstruktor), została w niezmienionej postaci względem klasy, którą właśnie rozszerzamy, po prostu pomijamy jej kod:


class Shape {
    constructor(x, y, side) { ... }
    area() { ... }
    calculate() { ... }
    radius() { ... }
}

class Triangle extends Shape {
    constructor(x, y, side) {
        super(x, y, side);
        this.type = "triangle";
    }
    area() { ... }

    //metody calculate() i radius() będą brane z powyższej klasy
}

Jak widzisz na powyższym przykładzie, zaczynają nam się pojawiać powtórki parametrów dla konstruktorów. Jeżeli będzie ich więcej, takie wypisywanie wszystkiego po kolei może być niewygodne. Dość często w takich sytuacjach stosuje się rest i spread:


class Shape {
    constructor(x, y, side) { ... }
    area() { ... }
    calculate() { ... }
    radius() { ... }
}

class Triangle extends Shape {
    constructor(...param) { //zbieramy za pomocą rest jako tablicę
        super(...param); //którą poniżej rozbijamy na części
        this.type = "triangle";
    }
    area() { ... }
}

const rect = new Triangle(10, 20, 30);

Przy czym nie zawsze będzie to najwygodniejsze rozwiązanie, ponieważ edytor przestanie nam podpowiadać nazwy parametrów jakie powinniśmy podać przy tworzeniu nowych instancji. Innym rozwiązaniem jest zastąpienie wielu parametrów pojedynczym obiektem z opcjami (1, 2).

Podobne działania do powyższych dość często będziesz widział, jeżeli będziesz spotykał się z komponentami klasowymi w React. Poniżej przykład z ich strony, który - jeżeli oczywiście będziesz używał takich komponentów - zawsze będzie zaczynał się bardzo podobnie - czyli od składni class Moj extends React.Component:


import React from "react";

class Clock extends React.Component {
    constructor(props) {
        super(props);
        this.state = {date: new Date()};
    }

    render() {
        return (
            <div>
                <h1>Hello, world!</h1>
                <h2>It is {this.state.date.toLocaleTimeString()}.</h2>
            </div>
        );
    }
}

Autorzy tej biblioteki stworzyli dla nas między innymi klasę Component (która jest jednym z elementów React), która zawiera kod bazowego komponentu. Ma ona swój konstruktor (który powyżej rozszerzamy poprzez super()), ale ma też kilka metod. Jedną z nich jest render(), którą my przesłaniamy tworząc zupełnie nowy kod.

Przykładów w necie masz miliony. Ot chociażby tutaj i tutaj.

Klasa abstrakcyjna

Klasa abstrakcyjna, to taka klasa, na bazie której nie powinniśmy tworzyć nowych instancji, a jedynie ją w przyszłości rozszerzać.

Posłużę się tutaj grafiką z rozdziału o dziedziczeniu:

dziedziczenie

Podczas tworzenia gry na planszy mógłby by sie pojawić obiekty typu EnemyFast, EnemyStrong i EnemyShoot, natomiast tworzenie pojedynczych instancji na bazie klasy Enemy było by bez sensu, ponieważ nie ma ona wystarczających informacji (np. grafiki). Klasa ta służy więc tylko do nadania podstawowych funkcjonalności, które przejmą klasy rozszerzające ją.

W powyższych kodach nic nie stało na przeszkodzie, by instancje tworzyć na podstawie każdej klasy.

Jeżeli wiemy, że dana klasa jest abstrakcyjną i chcielibyśmy dodatkowo zapobiec tworzeniu nowych instancji na bazie danej klasy, możemy posłużyć się zapisem:


class Animal {
    constructor(name) {
        if (this.constructor === Animal) {
            throw new Error("Nie możesz tworzyć obiektów z klasy abstrakcyjnej!");
        }
        this.type = "animal";
        this.name = name;
    }
    eat() {
        return "I eat food";
    }
}


//lub
class Animal {
    constructor(name) {
        if (new.target === Animal) {
            throw new Error("Nie możesz tworzyć obiektów z klasy abstrakcyjnej!");
        }
        this.type = "animal";
        this.name = name;
    }
    eat() {
        return "I eat food";
    }
}

Rozszerzanie wbudowanych typów

Tworzenie własnych typów danych na bazie wbudowanych w starszych wersjach Javascript nie było trywialną sprawą, co pokazują liczne wątki i artykuły. Na szczęście zmieniło się to wraz z wprowadzeniem klas:


class MyArray extends Array {
    constructor(...param) {
        super(...param);
    }

    sortNr() {
        return this.sort((a, b) => a - b);
    }
}

const tab1 = new Array(4, 5, 20, 21, 2.1, 1.2, 3);
tab1.sortNr(); //błąd : nie ma takiej metody

const tab2 = new MyArray(4, 5, 20, 21, 2.1, 1.2, 3);
tab2.sortNr();
console.log(tab2); //[1.2, 2.1, 3, 4, 5, 20, 21]

class MyString extends String {
    constructor(...param) {
        super(...param);
    }
    mix() {
        return [...this].map((letter, i) => (i % 2 === 0) ? letter.toUpperCase() : letter.toLowerCase()).join("");
    }
}

const txt1 = new String("lubie koty i psy");
txt1.mix(); //błąd : nie ma takiej metody

const txt = new MyString("lubie koty i psy");
console.log(txt.mix()); //LuBiE KoTy i pSy

Trening czyni mistrza

Jeżeli chcesz sobie potrenować zdobytą wiedzę z tego działu, to zadania znajdują się w repozytorium pod adresem: https://github.com/kartofelek007/zadania-es6

W repozytorium jest branch "solutions". Tam znajdziesz przykładowe rozwiązania.

Wszelkie prawa zastrzeżone. Jeżeli chcesz używać jakiejś części tego kursu, skontaktuj się z autorem.
Aha - i ta strona korzysta z ciasteczek.

Menu