Class - rozszerzanie typów

W poprzednich rozdziałach poznaliśmy konstruktory i klasy. Tworzone za ich pomocą obiekty dziedziczyły funkcjonalności ze swoich prototypów, a ich prototypy dziedziczyły ze swoich. Taka drabinka dziedziczenia tworzona była dla nas w zasadzie automatycznie.

My jako programiści możemy też sprawić, by dany typ dziedziczył z innego typu odpowiednio go rozszerzając.

Rozszerzanie typów

Aby rozszerzyć dany typ obiektów, skorzystamy z instrukcji extends:


class Enemy { //to może być też konstruktor
    constructor(x, y) {
        this.x = x;
        this.y = y;
        this.type = "normalny";
    }

    fly() {
        return this.type + " - lecę";
    }
}

class EnemyShooter extends Enemy {
    constructor(x, y) {
        super(x, y);
        this.type = "strzelający";
    }

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

const enemyNormal = new Enemy(10, 20);
console.log(enemyNormal.fly()); //"normalny - lecę"
console.log(enemyNormal.shoot()); //błąd - nie ma takiej metody

const enemyShooter = new EnemyShooter(20, 30);
console.log(enemyShooter.fly()); //"strzelający - lecę"
console.log(enemyShooter.shoot()); //"strzelający - strzelam"

W konstruktorze klasy rozszerzającej pojawiła nam się instrukcja super(), która w przypadku konstruktora służy do wywołania kodu z konstruktora klasy którą 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ę metody, którą chcemy rozszerzyć:


class Enemy {
    constructor(x, y) {
        this.x = x;
        this.y = y;
        this.type = "normalny";
    }

    fly() {
        return this.type + " - lecę";
    }
}

class EnemyShooter extends Enemy {
    constructor(x, y) {
        super(x, y);
        this.type = "strzelający";
    }

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

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

const enemyNormal = new Enemy(10, 20);
enemyNormal.fly(); //"normalny - lecę"

const enemyShooter = new EnemyShooter(20, 30);
console.log(enemyShooter.fly()); //"strzelający - lecę i czasami strzelam"

I tutaj ponownie - gdyby oryginalna 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} x ${y}`
    }
}

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

const enemyNormal = new Enemy(10, 20);
enemyNormal.flyTo(100, 200); //"Lecę do pozycji 100 x 200"

const enemyShooter = new EnemyShooter(20, 30);
console.log(enemyShooter.flyTo(100, 200)); //"Lecę do pozycji undefined x undefined i strzelam"

Jeżeli chcemy daną metodę przesłonić (czyli by nic nie dziedziczyła z klasy rozszerzanej) zwyczajnie nie korzystamy z instrukcji super():


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

class enemyStatic extends Enemy {
    flyTo() {
        //nie używamy super.flyTo(x, y);
        return "Ten typ obiektów w ogóle nie lata";
    }
}

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 klasy Shape
}

Podobne do powyższych operacji możemy też wykonać za pomocą konstruktorów. Niestety w przypadku tamtej składni kod jest bardziej skomplikowany, a i mamy mniejsze możliwości działania, ponieważ nie możemy swobodnie rozszerzać wbudowanych typów. W dzisiejszych czasach do rozszerzania typów polecam pozostać przy klasach. Jeżeli chcesz zobaczyć, jak to wyglądało, spójrz na ten przykład.


    function Enemy(x, y) {
        this.x = x;
        this.y = y;
        this.type = "normalny";
    }
    Enemy.prototype.fly = function() {
        return this.type + " - lecę";
    }

    function EnemyShooter(x, y) {
        //odpalam kod konstruktora z klasy rozszerzanej. W składni class to super(x, y);
        Enemy.call(this, x, y);
        this.type = "strzelający";
    }

    //tutaj muszę ręcznie ustawić odpowiedni prototyp dla klasy rozszerzającej
    EnemyShooter.prototype = Object.create(Enemy.prototype);
    EnemyShooter.prototype.constructor = EnemyShooter;

    EnemyShooter.prototype.fly = function() {
        //odpalam funkcję z klasy rozszerzanej przekazując do niej odpowiednie this
        const txt = Enemy.prototype.fly.call(this);
        return txt + " i czasami strzelam";
    }

    const enemyNormal = new Enemy(10, 20);
    console.log(enemyNormal.fly()); //"normalny - lecę"
    console.log(enemyNormal.shoot()); //błąd - nie ma takiej metody

    const enemyShooter = new EnemyShooter(20, 30);
    console.log(enemyShooter.fly()); //"strzelający - lecę i czasami strzelam"
    

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

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.