Kihagyás

A gyakorlat anyaga

Ezen a gyakorlaton megismerkedünk a JavaScript nyelven történő objektumorientált programozással.

Osztályok, objektumok

A korábban megismert objektumorientált paradigma alapvető elemei az osztályok. Ezek tulajdonképpen azonos tulajdonságokkal és viselkedéssel rendelkező objektumok általánosításai, formai leírásai.

Érdekes módon a JavaScriptben nem mindig voltak osztályok. Az osztály, mint beépített nyelvi elem csupán a 2015-ös ECMAScript6 (ES6) szabványban jelent meg. Arról, hogy az osztályok bevezetése előtt milyen megoldások voltak JavaScriptben az objektumok általános formai leírásának elkészítésre, az előadáson lesz szó.

Osztályok létrehozása

A fentebb említett ECMAScript6 szabvány bevezette a class kulcsszót, aminek segítségével osztályokat hozhatunk létre JavaScriptben.

Példa: Egy Vehicle nevű osztály létrehozása

1
2
3
class Vehicle {
    // class body comes here...
}

Adattagok, metódusok

Egy osztálynak lehetnek adattagjai, illetve metódusai (tagfüggvényei). Ezekre JavaScriptben a . (pont) operátorral tudunk hivatkozni.

Az osztály metódusait a függvényekhez hasonló módon hozhatjuk létre, azzal a fontos különbséggel, hogy a függvények definiálásakor használt function kulcsszót elhagyjuk.

Példa: Egy paraméter nélküli info() és egy egyparaméteres boardPassenger() metódus.

1
2
3
4
5
6
7
8
9
class Vehicle {
    info() {
        console.log("This is a vehicle");
    }

    boardPassenger(passengerName) {
        console.log("Our new passenger: " + passengerName);
    }
}

Pythonhoz hasonlóan, JavaScriptben sem deklaráljuk az adattagokat külön az osztályban. Az adattagok létrehozása és inicializálása itt is a konstruktorban történik.

Konstruktor

JavaScriptben a constructor() névre hallgató speciális metódus lesz az osztály konstruktora. Ez természetesen akkor kerül meghívásra, amikor az osztályt példányosítjuk. A konstruktort használjuk az osztály adattagjainak létrehozására és inicializálására.

Amennyiben az osztályunkba nem írunk konstruktort, akkor az interpreter automatikusan gyártani fog egy paraméter nélküli konstruktort (default konstruktort), ami az osztály példányosításakor fog meghívódni.

JavaScriptben a this kulcsszóval hivatkozhatunk az aktuális objektumra. Ha egy osztályon belül hivatkozni szeretnénk egy adattagra vagy egy metódusra, akkor a kérdéses adattag vagy metódus neve elé mindig kötelezően ki kell írni a this kulcsszót!

Példa: Egy két paraméteres konstruktor, amely paraméterei alapján inicializáljuk a brand és speed adattagokat.

1
2
3
4
5
6
class Vehicle {
    constructor(brand, speed) {
        this.brand = brand;
        this.speed = speed;
    }
}

JavaScriptben továbbra sincs function overload. Ha azt szeretnénk elérni, hogy egy metódust többféle, eltérő paraméterezéssel is tudjunk használni, akkor használjuk a default függvényparamétereket!

Példa: Írjuk át a Vehicle osztály konstruktorát úgy, hogy a speed paraméter értékét ne legyen kötelező megadni, alapértéke legyen 0!

1
2
3
4
5
6
class Vehicle {
    constructor(brand, speed = 0) {
        this.brand = brand;
        this.speed = speed;
    }
}

Példa: Egészítsük ki az osztályunkat a következőkkel:

  • A konstruktorban hozzunk létre egy passengers adattagot, amit egy üres tömbbel inicializáljunk!
  • Írjuk meg a boardPassenger() metódust, amely egy utas nevét (szöveges adat) várja paraméterül! A metódus szúrja be a paraméterül kapott utas nevét a passengers tömb végére!
  • Készítsük el az info() metódust, ami kiírja a jármű márkáját, sebességét és az utasok számát!
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
class Vehicle {
    constructor(brand, speed = 0) {
        this.brand = brand;
        this.speed = speed;
        this.passengers = [];  // new data member, initialized with empty array
    }

    boardPassenger(passengerName) {
        if (typeof passengerName === "string") {  // type checking
            this.passengers.push(passengerName);  // insert new passenger into array
        }
    }

    info() {
        console.log(`${this.brand} vehicle, with speed ${this.speed} km/h, carrying ${this.passengers.length} passengers.`);
    }
}

Class fields (modern módszer)

Modern JavaScriptben közvetlenül az osztály törzsében is deklarálhatunk adattagokat, nem csak a konstruktorban:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
class Vehicle {
    // Class fields - declared directly in class body
    passengers = [];
    isActive = true;

    constructor(brand, speed = 0) {
        this.brand = brand;
        this.speed = speed;
        // passengers already exists, no need to initialize
    }

    boardPassenger(passengerName) {
        if (typeof passengerName === "string") {
            this.passengers.push(passengerName);
        }
    }
}

Előnyök: - Egyértelműbb, hogy az osztálynak milyen adattagjai vannak - Nem kell minden adattagot a konstruktorban inicializálni - Jobb olvashatóság

Objektumok létrehozása

A példányosítás során az osztályból egy objektumpéldányt hozunk létre.

Az osztály példányosítása, avagy egy új objektum létrehozása JavaScriptben a new kulcsszó segítségével történik. A példányosításkor meghívjuk az osztály konstruktorát, és átadjuk neki a megfelelő paramétereket (ha vannak).

Példa: A Vehicle osztály példányosítása, metódusok meghívása

 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
class Vehicle {
    constructor(brand, speed = 0) {
        this.brand = brand;
        this.speed = speed;
        this.passengers = [];
    }

    boardPassenger(passengerName) {
        if (typeof passengerName === "string") {
            this.passengers.push(passengerName);
        }
    }

    info() {
        console.log(`${this.brand} brand vehicle, with speed ${this.speed} km/h, carrying ${this.passengers.length} passengers.`);
    }
}

// instantiation + testing methods

const vehicle1 = new Vehicle("Skoda", 90);
const vehicle2 = new Vehicle("Ikarus");  // speed takes default value 0

vehicle1.boardPassenger("Uncle Joe");
vehicle1.info();
vehicle2.info();

Kimenet

Skoda brand vehicle, with speed 90 km/h, carrying 1 passengers.

Ikarus brand vehicle, with speed 0 km/h, carrying 0 passengers.

Láthatóságok, getterek, setterek

A Pythonhoz hasonló módon JavaScriptben sem tudtuk szabályozni módosítószavakkal az adattagok láthatóságát (vesd össze: Java). Alapértelmezett módon minden adattag publikus, azaz mindenhonnan elérhető.

Konvenció alapján az adattag neve előtti egyszeres alulvonás jelzi azt, hogy az adattag nem publikus használatra van szánva ("private adattag"). Viszont ettől az adattag kívülről továbbra is elérhető lesz!

Példa: Jelezzük, hogy a márka és sebesség adattagokat nem publikus használatra szánjuk!

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
class Vehicle {
    constructor(brand, speed = 0) {
        this._brand = brand;      // data members not intended for public use
        this._speed = speed;
        this.passengers = [];
    }

    boardPassenger(passengerName) {
        if (typeof passengerName === "string") {
            this.passengers.push(passengerName);
        }
    }

    info() {
        console.log(`${this._brand} brand vehicle, with speed ${this._speed} km/h, carrying ${this.passengers.length} passengers.`);
    }
}

Private fields (modern megoldás)

A modern JavaScriptben (ES2022+) valódi privát mezőket hozhatunk létre a # szimbólum használatával:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class Vehicle {
    #brand;      // truly private field
    #speed;
    passengers = [];

    constructor(brand, speed = 0) {
        this.#brand = brand;
        this.#speed = speed;
    }

    getSpeed() {
        return this.#speed;
    }

    info() {
        console.log(`${this.#brand} brand vehicle, with speed ${this.#speed} km/h`);
    }
}

const vehicle = new Vehicle("BMW", 120);
vehicle.info();                    // works
console.log(vehicle.getSpeed());   // works
// console.log(vehicle.#speed);    // ERROR! Private field cannot be accessed outside class

Ez azonban csak ES2022+ böngészőkben és interpreterekben működik. Ebből kifolyólag, ha ezt használjuk, kompabilitási gondok léphetnek fel!

Getterek és setterek

Ha a nem publikus használatra szánt adattagok szabályozott elérését és módosítását szeretnénk elérni JavaScriptben, akkor készíthetünk ezekhez az adattagokhoz gettereket, illetve settereket.

JavaScriptben a gettert, valamint a settert property-ként valósíthatjuk meg. A get property-t a get, míg a set property-t a set kulcsszóval hozhatjuk létre. A property-k használatával szabályozott módon kérhetjük le és állíthatjuk be adattagok értékét úgy, hogy kívülről azt a látszatot keltjük, mintha új, publikus adattagokkal dolgoznánk.

Példa: Készítsünk gettert és settert az _speed adattaghoz!

 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
28
class Vehicle {
    constructor(brand, speed = 0) {
        this._brand = brand;
        this._speed = speed;
        this.passengers = [];
    }

    get speed() {
        return this._speed;
    }

    set speed(newValue) {
        // in the setter we can perform all kinds of validations...

        if (typeof newValue === "number" && newValue >= 0) {
            this._speed = newValue;
        } else {
            console.log("Speed value can only be a non-negative number!");
        }
    }

    // ...
}

const vehicle = new Vehicle("Lada", 50);
vehicle.speed = 60;             // setter call
vehicle.speed = -100;           // setter call (invalid data)
console.log(vehicle.speed);     // getter call

Kimenet

Speed value can only be a non-negative number!

60

Figyelem

Fontos, hogy a property és az adattag neve mindig eltérő legyen, különben végtelen rekurzióba futunk! A példában a property neve speed (alulvonás nélkül), az adattag neve ettől eltérő módon _speed (alulvonással).

1
2
3
4
5
6
7
8
9
class Wrong {
    constructor(value) {
        this.value = value;
    }

    get value() {
        return this.value;  // WRONG! Calls itself -> infinite recursion!
    }
}

Statikus metódusok és property-k

A statikus metódusok és property-k az osztályhoz tartoznak, nem az egyes példányokhoz. Ezeket a static kulcsszóval hozhatjuk létre.

 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
class MathHelper {
    static PI = 3.14159;
    static E = 2.71828;

    static add(a, b) {
        return a + b;
    }

    static multiply(a, b) {
        return a * b;
    }

    static circleArea(radius) {
        return this.PI * radius * radius;  // can use static property
    }
}

// Call static methods on the class itself, not on instances
console.log(MathHelper.add(5, 3));           // 8
console.log(MathHelper.PI);                  // 3.14159
console.log(MathHelper.circleArea(10));      // 314.159

// Cannot call on instances
const helper = new MathHelper();
// helper.add(5, 3);  // ERROR! add is not a method on instances

Persze ehhez hasonló osztály létezik, így nem szükséges ezt elkészítenünk.

Gyakorlati példa a Vehicle osztállyal:

 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
28
29
30
31
32
33
34
35
36
37
38
39
class Vehicle {
    static vehicleCount = 0;  // track total number of vehicles created

    constructor(brand, speed = 0) {
        this._brand = brand;
        this._speed = speed;
        this.passengers = [];
        Vehicle.vehicleCount++;  // increment static counter
    }

    static compareSpeed(vehicle1, vehicle2) {
        return vehicle1._speed - vehicle2._speed;
    }

    static getTotalVehicles() {
        return Vehicle.vehicleCount;
    }

    get speed() {
        return this._speed;
    }

    set speed(value) {
        if (typeof value === "number" && value >= 0) {
            this._speed = value;
        }
    }
}

const v1 = new Vehicle("BMW", 120);
const v2 = new Vehicle("Audi", 110);
const v3 = new Vehicle("Mercedes", 130);

console.log(Vehicle.getTotalVehicles());     // 3
console.log(Vehicle.compareSpeed(v1, v2));   // 10 (v1 is faster)

// Sort vehicles by speed
const vehicles = [v1, v2, v3];
vehicles.sort(Vehicle.compareSpeed);

Öröklődés

A korábbi tanulmányainkból ismerős lehet számunkra az öröklődés fogalma. Ez egy osztályok közötti kapcsolat, amely egy úgynevezett ősosztály (szülőosztály) és egy gyermekosztály (leszármazott osztály) között valósul meg. A gyermekosztály tulajdonképpen az ősosztályának egy speciális változata lesz (pl. a Dog osztály az Animal osztály gyermeke, hiszen minden kutya egyben állat is).

Az öröklődés során a gyermekosztály megörökli az ősosztályának összes adattagját és metódusát. A gyermekosztályban létrehozhatunk az örökölt adattagokon kívül még egyéb adattagokat, metódusokat, illetve lehetőségünk van az örökölt metódusok működésének felüldefiniálására is (overriding).

JavaScriptben az öröklődést a következő szintaxissal adhatjuk meg:

1
2
3
class ChildClass extends ParentClass {
    // child class body...
}

Fontos megjegyezni, hogy JavaScriptben csak egyszeres öröklődés van, tehát egy osztálynak nem lehet kettő vagy több ősosztálya.

A gyermekosztályból hivatkozhatunk az ősosztályra, annak adattagjaira és metódusaira a super kulcsszóval. Ennek segítségével meghívhatjuk az ősosztály konstruktorát az adott osztályon belül. Ha a gyermekosztályban nem hozunk létre új adattagot, akkor az ősosztály konstruktorának meghívása elhagyható.

Példa: Hozzunk létre egy Car osztályt, ami az imént elkészített Vehicle osztályból származik!

 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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
class Vehicle {
    constructor(brand, speed = 0) {
        this._brand = brand;
        this._speed = speed;
        this.passengers = [];
    }

    get brand() {
        return this._brand;
    }

    set brand(value) {
        this._brand = value;
    }

    get speed() {
        return this._speed;
    }

    set speed(value) {
        if (typeof value === "number" && value >= 0)
            this._speed = value;
        else
            console.log("Speed value can only be a non-negative number!");
    }

    boardPassenger(passengerName) {
        if (typeof passengerName === "string")
            this.passengers.push(passengerName);
    }

    info() {
        console.log(`${this._brand} brand vehicle, with speed ${this._speed} km/h, carrying ${this.passengers.length} passengers.`);
    }
}

// child class

class Car extends Vehicle {  // car is a special vehicle
    constructor(brand, speed = 0, selfDriving = false) {
        super(brand, speed);           // call parent class constructor
        this.selfDriving = selfDriving;  // new data member not in parent
    }

    info() {  // overriding: redefine the info() method inherited from parent
        console.log(`${this.brand} car, (speed: ${this.speed} km/h), passengers: ${this.passengers.length}, self-driving: ${this.selfDriving ? "yes" : "no"}`);
    }

    honk() {  // new method definition
        console.log("BEEP BEEP!");
    }
}

// instantiation

const car1 = new Car("Trabant", 40, false);
const car2 = new Car("Tesla", 130, true);

car2.boardPassenger("Elon Musk");  // this method comes from parent class
car2.boardPassenger("Bill Gates");

car1.honk();
car1.info();
car2.info();

Kimenet

BEEP BEEP!

Trabant car, (speed: 40 km/h), passengers: 0, self-driving: no

Tesla car, (speed: 130 km/h), passengers: 2, self-driving: yes

Super kulcsszó használata metódusokban

A super kulcsszóval nem csak a konstruktort hívhatjuk meg, hanem az ősosztály bármely metódusát:

 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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
class Vehicle {
    constructor(brand) {
        this.brand = brand;
    }

    start() {
        console.log(`${this.brand} is starting...`);
    }

    stop() {
        console.log(`${this.brand} is stopping...`);
    }
}

class Car extends Vehicle {
    constructor(brand, engineType) {
        super(brand);
        this.engineType = engineType;
    }

    start() {
        console.log("Checking fuel level...");
        super.start();  // call parent's start method
        console.log(`Engine type: ${this.engineType}`);
    }

    stop() {
        console.log("Engaging parking brake...");
        super.stop();  // call parent's stop method
    }
}

const car = new Car("Toyota", "Hybrid");
car.start();
/*
Output:
Checking fuel level...
Toyota is starting...
Engine type: Hybrid
*/

car.stop();
/*
Output:
Engaging parking brake...
Toyota is stopping...
*/

Method chaining (fluent interface)

A this visszaadásával lehetővé tesszük a metódushívások láncolását:

 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
28
29
30
31
32
33
class Vehicle {
    constructor(brand) {
        this.brand = brand;
        this._speed = 0;
        this.passengers = [];
    }

    setSpeed(speed) {
        this._speed = speed;
        return this;  // return this for chaining
    }

    boardPassenger(name) {
        this.passengers.push(name);
        return this;  // return this for chaining
    }

    info() {
        console.log(`${this.brand}: ${this._speed} km/h, ${this.passengers.length} passengers`);
        return this;  // return this for chaining
    }
}

// Method chaining in action
const vehicle = new Vehicle("Bus");
vehicle
    .setSpeed(60)
    .boardPassenger("Alice")
    .boardPassenger("Bob")
    .boardPassenger("Charlie")
    .info()  // Bus: 60 km/h, 3 passengers
    .setSpeed(80)
    .info(); // Bus: 80 km/h, 3 passengers

Típusellenőrzés

Amint azt korábban láthattuk, a typeof operátorral le tudjuk kérni, hogy egy adott objektum adott típusú-e.

1
2
console.log(typeof 42 === "number");   // true
console.log(typeof 42 === "string");   // false

Fontos megjegyezni, hogy a typeof csak beépített típusokra működik! Ha egy saját osztályból példányosított objektumról szeretnénk eldönteni, hogy adott típusú-e, akkor az instanceof operátor használatos. Az obj instanceof ClassName visszaadja, hogy az obj objektum ClassName osztály példánya-e.

 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 Vehicle {
    constructor(brand, speed = 0) {
        this._brand = brand;
        this._speed = speed;
        this.passengers = [];
    }
}

class Car extends Vehicle {
    constructor(brand, speed = 0, selfDriving = false) {
        super(brand, speed);
        this.selfDriving = selfDriving;
    }
}

const vehicle = new Vehicle("Apache helicopter", 100);
const car = new Car("Volkswagen", 70, false);

console.log(vehicle instanceof Vehicle);  // true
console.log(car instanceof Vehicle);      // true (car is also a vehicle!)
console.log(vehicle instanceof Car);      // false
console.log(car instanceof Car);          // true

// Also works with built-in types
console.log([] instanceof Array);         // true
console.log({} instanceof Object);        // true
console.log("hello" instanceof String);   // false (primitive, not object)