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
).
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.
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.
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]] |
+---------------+
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.
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.
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.
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"
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.