Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
Objektum orientált programozás JavaScriptben

Objektum orientált programozás JavaScriptben

A következő cikksorozattal szeretném folytatni a JavaScript nyelv bemutatását, amit a függvények bemutatásával kezdtem el. A további cikkekben szeretnék jobban elmélyedni a további témákba, mint

  • funkcionális programozás,
  • eseménykezelés,
  • objektumok
  • valamint kitérni arra, hogyan változnak meg az ECMAScript (ES) következő változataiban.

Jelen esetben korszerű ES5 kompatibilis JavaScript motort feltételezek, de kitérek arra, hogyan lehet megvalósítani mindezt régebbi böngészőkben is (amennyiben egyáltalán lehetséges).

A JavaScript egy objektum orientált programozási (OOP) nyelv. Egy prototípusos nyelv, ami azt jelenti, hogy az öröklődést prototípusok segítségével és nem osztályokkal valósítja meg. JavaScriptben minden objektum vagy automatikusan objektummá alakul amikor szükséges (kivéve a null és az undefined).

Primitívek

JavaScriptben 5 primitív típus van (boolean, number, string, null és undefined) és az utolsó kettő kivételével automatikusan objektummá alakulnak, amikor meghívjuk egy metódusukat, majd automatikusan vissza is alakulnak primitívvé.

false.toString() === "false" // true
"string".slice(2).replace("r", "k") === "king"; // true
(typeof 1) === "number"; // true

A primitívek immutable-ok, azaz nem változhat az értékük. Ugyanakkor becsomagolhatjuk őket egy objektumba a becsomagoló függvényükkel: a Boolean, Number, és String függvényt a new operátorral meghívva (vagy az Object függvényt használva) és átadva neki a primitív értéket:

new Boolean(true) === true; // false
new Boolean(true).valueOf() === true; // true
(typeof (new Number(1))) === "object" // true
Object("string") instanceof String // true

A becsomagolt primitív mostmár egy teljes értékű objektum, kicsomagolni a valueOf metódus meghívásával tudjuk. Mivel nincs igazolhatóan megfelelő eset a használatukra, legjobb ha kerüljük a becsomagolást és kihasználjuk, hogy a primitív értéket a futtatókörnyezet automatikusan be- és kicsomagolja.

Objektumok

Ahogy korábban írtam, a JavaScriptben minden (a primitívek kivételével) objektum (hasonlóan a Javahoz). Ugyanakkor a Javaval ellenttben itt minden objektum egy kulcs-érték pár tároló. Objektumokat nagyon egyszerűen tudunk létrehozni: az Objektum konstruktor segítségével illetve objektum literál használatával (ez utóbbi az ajánlott):

var o1 = new Object();
var o2 = {};

Több nyelvvel ellentétben további tulajdonságokat adhatunk egy objektumhoz a létrehozás után is, a pont szintaxissal illetve szögletes zárójel használatával:

var o3 = {};
o2.poperty1 = "value1";
o2["property2"] = true;

A tulajdonságok nevei sztringek (ES6-ban [ES 2015-ben] kiegészülve a Symbol típussal) és akármilyen értéket tárolhatnak: primitívet és objektumot is. Az értékeket már az objektum literál felírásakor is megadhatjuk:

var o4 = { 
  property3: 1, 
  "continue": function () { 
    return null;
  }
};

OOP-ben egy függvényt metódusnak nevezünk, ha az egy objektum tulajdonsága. Mivel minden függvény a JavaScriptben egyben objektum is, így természetesen megadható tulajdonságnak is, így lesznek az objektumoknak metódusai.

Prototípus lánc

Minden objektumnak JavaScriptben van egy prototípusa, a [[Prototype]], ami közvetlenül nem hozzáférhető, habár néhány futtatómotor elérhetővé teszi a __proto__ tulajdonságon keresztül. Ha az Object konstruktort vagy az objektum literált használjuk objektumunk létrehozására, akkor objektumunk [[Prototype]] tulajdonsága az Object prototype objektumára fog mutatani (és ezzel megörököl néhány tulajdonságot tőle, mint amilyen a toString és a valueOf).

Amikor egy objektum egy tulajdonságát akarjuk lekérdezni, akkor a futtató motor előbb megkeresi az objektum saját tulajdonságai között. Ha itt nem találja, akkor feljebb lép a prototípus láncon (úgy hogy lekérdezi az objektum [[Prototype]] értékét) és itt fogja keresni a tulajdonságot, egészen addig amíg el nem éri a null-t a prototípus lánc végén.

+---------------+  
|      null     |
+---------------+
        ^
        |
+---------------+
|     Object    |
+---------------+
| [[Prototype]] |
+---------------+
        |
       ...
        |
+---------------+
|    MyObject   |
+---------------+  
| [[Prototype]] |
+---------------+

Öröklődés

JavaScriptben az öröklődést a prototípus lánc kiterjesztésével oldjuk meg hagyományosan. Egy új elem létrehozásával az előbb leírt prototípus lánc végéhez adunk egy új elemet. Például a var o5 = {}; a következő prototípus láncot eredményezi:

+---------------+
|      null     |
+---------------+
        ^
        |
+---------------+
|     Object    |
+---------------+
| [[Prototype]] |
+---------------+
        ^
        |
+---------------+
|       o5      |
+---------------+
| [[Prototype]] |
+---------------+

Felülírhatjuk az örökölt tulajdonságokat, vagy hozzáadhatunk újakat, magában az objektumban, illetve bárhol a prototípus láncban. A prototípus láncban való felülírás azt jelenti, hogy minden objektum, aminek a prototípus lánca tartalmazza az objektumot, szintén megkapja az új tulajdonságot. A nyelv ezen tulajdonságát egy hatalmas előnynek tekinthetjük, ugyanakkor rengeteg hiba okozója is lehet.

Object.create

Az Object.create segítségével könnyen megvalósíthatjuk az öröklődést (mellékhatások nélkül):

var ObjectProto = {
  name: "ObjectProto",
  sayName: function() { 
    console.log("My name is: " + this.name);
  }
};

ObjectProto.sayName(); // "My name is: ObjectProto"

var myObject = Object.create(ObjectProto);
myObject.name = "myObject";
myObject.sayName(); // "My name is: myObject"

Habár a kódunkban nem deklaráltuk a myObject.sayName metódust, meghívásakor a motor kikeresi az ObjectProto objektumban megtalálhatót a prototípus láncban való lépkedés segítségével.

A megmaradt problémánk, hogy még egy sor kódot kell írnunk a name tulajdonság felülírásához. Ezt megoldhatjuk úgy, hogy átadunk egy propertiesObject objektumot az Object.create-nek:

var myObject2 = Object.create(ObjectProto, {
  name: {
    value: "myObject2", 
    enumerable: true, 
    writable: true, 
    configurable: true 
  }
});
myObject2.sayName() // "My name is: myObject2"

Azokban az implementációkban, amik nem valósították meg az Object.create függvényt, egy polyfill használatával könnyen megadhatjuk az alapvető működést (a propertiesObject használatának kivételével):

if (typeof Object.create != 'function') {
  Object.create = (function() {
    var Temp = function() {};
    return function (prototype) {
      if (arguments.length > 1) {
        throw Error('Second argument not supported');
      }
      if (typeof prototype != 'object') {
        throw TypeError('Argument must be an object');
      }
      Temp.prototype = prototype;
      var result = new Temp();
      Temp.prototype = null;
      return result;
    };
  })();
}

Az Object.create használatával még mindig ránk marad az objektumok létrehozásának automatizálása. Itt jönnek segítségünkre a konstruktorok.

Konstruktorok

A konstruktorok szokásos függvények. Viszont amikor a new operátorral hívjuk meg őket, akkor válnak konstruktor függvényekké és visszaadnak egy objektumot (még akkor is, ha semmilyen értéket, vagy primitíved adnának is vissza a return utasítással). A függvényeket, amiket konstruktorként szeretnénk használni, olyanra érdemes tervezni, hogy úgy nézzenek ki, és úgy is viselkedjeken, mint egy konstruktor. Ezért legjobb ha nagybetűvel kezdjük a nevét, ezzel jelezve a használójának, mi a fejlesztő szándéka:

function Shape() {
  console.log("Constructor called");
}

var shape = new Shape(); // A () jelek opcionálisak ha egy függvényt konstruktorként hívunk meg.
// > "Constructor called"
console.log(typeof shape);
// > "object"

Közvetlenül a konstruktorfüggvény meghívása előtt a JavaScript motor létrehoz egy objektumot, melynek [[Prototype]]-ja a a konstruktor prototípusára mutat. Ezen új objektumot ezután a függvény a this értékeként kapja meg. Ha a függvény nem ad vissza semmit, vagy nem objektumot ad vissza, a this értéke kerül automatukusan visszaadásra.

Kiterjeszthetjük az objektumunkat úgy, hogy újabb tulajdonságokat adunk a konstruktora prototype objektumához akár azután is, hogy az objektum létrejött:

Shape.prototype.move = function () {
  console.log("Shape moved");
};
shape.move() // "Shape moved"

Minden objektum, amit a konstruktor fügvénnyel hoztunk létre, ezután megörökli az összes tulajdonságot, amit a prototype-hoz adunk, mivel azok bekerülnek a prototípus láncba.

Öröklődés konstruktorokkal

Kombinálhatjuk az öröklődést és konstrukotor függvényeket. Ezzel olyan objektumokat nyerünk, melyek egymástól örökölnek tulajdonságokat. Ehhez újabb konstruktort hozunk létre, annak prototípusát megfeleltetjük a szülő konstruktorának prototype-jából létrejött objektumot, végül pedig helyreállítjuk a konstruktor prototípusának constructor tulajdonságát:

function Rectangle() {
}
Rectangle.prototype = Object.create(Shape.prototype);
Rectangle.prototype.constructor = Rectangle;

A constructor egy függvényre mutat, ami a prototípus példányait hozza létre. A JavaScript motor mikor deklarálunk egy függvényt, hozzáadja a prototípushoz a hivatkozást a függvényre. Miután az objektum prototípusát felülírtuk, vissza kell állítanunk a constructor értékét az eredetire. Ezután már használhatjuk is az új konstruktort. Az általa generált objektumok ezután mind a szülő mind pedig a konstruktor prototípusának tulajdonságait megöröklik:

var rect = new Rectangle();
console.log(rect instanceof Rectangle); // true
console.log(rect instanceof Shape); // true
rect.move(); // "Shape moved"

A szülő függvényeinek meghívása

Az előző megvalósításnak van egy kisebb hibája. Nem hívja meg a szülő (superclass) konstruktorát, amikor egy új objektumot hoz létre. Ezt viszonylag könnyű megjavítani. Meg kell hívni a szülőt konstruktor függvényének call metódusát, átadva neki az újonnan létrejött this objektumot, valamint a további paramétereket, amennyiben szükséges:

function Rectangle() {
  Shape.call(this);
}

Mivel a fenti esetben mindig emlékezni kell mi volt a szülőosztály konstruktora, ezért a megoldás nem túl elegáns, ezen kívül szorossá teszi a csatolást a szülő és a gyermek között, mivel a kódban többször kell, hogy szerepeljen a szülő konstruktorának a neve. Kicsit dinamikusabbá tudjuk tenni ezt a csatolást azzal, az öröklődést egy segédfügvénnyel valósítjuk meg. Pár függvénytár ad is nekünk ilyen kényelmi funkciót, mint amilyen a util.inherits Node.js-ben vagy a goog.inherits a Google Closure Library-ben. Segítségükkel az öröklődés és a szülő elérése átlátszóvá válik. Amennyiben nem használunk olyan függvénytárat, ami segít ebben, könnyen megvalósíthatjuk:

function inherits(childConstructor, superConstructor) {
  childConstructor.super_ = superConstructor;
  childConstructor.prototype = Object.create(superConstructor.prototype, {
    constructor: {
      value: childConstructor,
      enumerable: false,
      writable: true,
      configurable: true
    }
  });
}

Ezek után már használhatjuk is, hogy ezzel hozzuk létre a prototípus láncunkat, és meghívjuk a konstruktort és annak prototípusának metódusait:

function Shape() {
  console.log("Constructor called");
}

Shape.prototype.move = function () {
  console.log("Shape moved");
};

function Rectangle() {
  Rectangle.super_.call(this);
}
inherits(Rectangle, Shape);

Rectangle.prototype.move = function () {
  console.log("Rectangle moved");
  Rectangle.super_.prototype.move.call(this);
};

Ezek után, ha létrehozunk egy új Rectangle objektumot, és meghívjuk a move metódusát, akkor meg fogja hívni a szülő prototípusának move metódusát is. Mivel ezzel eltávolítottuk a közvetlen függést a szülőtől szinte minden helyen, könnyebbé vált lecserélni a szülőt a jövőben például egy Polygon típusra, amennyiben szükséges, anélkül, hogy mindenhol át kellene írni a hivatkozást.

Remélem sikerült felkeltenem az érdeklődést az OOP programozás témájában azokban is, akik nem igazán ismerték vagy használták korábban.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.