Bevezetés az objektum-orientált programozásba a JavaScript-ben: objektumok, prototípusok és osztályok

Számos programozási nyelvben az osztályok jól definiált fogalom. JavaScriptben nem ez a helyzet. Vagy legalábbis nem ez volt a helyzet. Ha az OOP-ra és a JavaScript-re keres, sok cikkbe ütközik, rengeteg különféle recepttel, hogy miként lehet utánozni a classJavaScript-et.

Van-e egyszerű, KISS módszer az osztály definiálására a JavaScript-ben? És ha igen, miért olyan sokféle recept az osztály meghatározásához?

Mielőtt válaszolna ezekre a kérdésekre, értsük meg jobban, mi is a JavaScript Object.

Objektumok a JavaScript-ben

Kezdjük egy nagyon egyszerű példával:

const a = {}; a.foo = 'bar';

A fenti kódrészletben egy objektumot hoznak létre és javítanak egy tulajdonsággal foo. A dolgok hozzáadásának lehetősége egy meglévő objektumhoz különbözteti meg a JavaScriptet a klasszikus nyelvektől, például a Java-tól.

Részletesebben: az a tény, hogy egy objektum tovább bővíthető, lehetővé teszi egy „implicit” osztály egy példányának létrehozását anélkül, hogy az osztályt valóban létre kellene hozni. Tisztázzuk ezt a fogalmat egy példával:

function distance(p1, p2) { return Math.sqrt( (p1.x - p2.x) ** 2 + (p1.y - p2.y) ** 2 ); } distance({x:1,y:1},{x:2,y:2});

A fenti példában nem volt szükségem Point osztályra a pont létrehozásához, csak kibővítettem az Objectösszeadás xés a ytulajdonságok példányát . A távolság függvény nem érdekli, hogy az argumentumok az osztály példányai-e Pointvagy sem. Amíg nem hívja meg a distancefüggvényt két olyan objektummal, amelyek típusú xés ytulajdonságúak Number, addig ez tökéletesen működik. Ezt a koncepciót néha kacsa tipizálásnak nevezik .

Eddig csak adatobjektumot használtam: olyan objektumot, amely csak adatokat tartalmaz, és nem tartalmaz funkciót. De a JavaScript-ben funkciók hozzáadhatók egy objektumhoz:

const point1 = { x: 1, y: 1, toString() { return `(${this.x},${this.y})`; } }; const point2 = { x: 2, y: 2, toString() { return `(${this.x},${this.y})`; } };

Ezúttal a 2D pontot képviselő objektumoknak van egy toString()módszerük. A fenti példában a toStringkódot lemásolták, és ez nem jó.

Sokféle módon lehet elkerülni ezt a duplikációt, és valójában a JS-ben található objektumokról és osztályokról szóló különféle cikkekben különböző megoldásokat talál. Hallottál már valaha a „Revealing module pattern” -ről? Ez tartalmazza a „minta” és a „leleplezés” szavakat, hűvösen hangzik, és a „modul” elengedhetetlen. Tehát biztosan megfelelő módon lehet objektumokat létrehozni ... csakhogy nem az. A modulminta feltárása bizonyos esetekben megfelelő választás lehet, de a viselkedésmódú objektumok létrehozásának alapértelmezett módja ez nem.

Most készen állunk az órák bevezetésére.

Osztályok JavaScript-ben

Mi az osztály? Szótárból: az osztály „olyan dolgok halmaza vagy kategóriája, amelyek valamilyen tulajdonsággal vagy tulajdonsággal rendelkeznek, és jellegük, típusuk vagy minőségük szerint megkülönböztethetők másoktól”.

A programozási nyelvekben gyakran azt mondjuk, hogy „Az objektum egy osztály példánya”. Ez azt jelenti, hogy egy osztály használatával sok objektumot hozhatok létre, és mindegyikük megosztja a módszereket és a tulajdonságokat.

Mivel az objektumok tovább fejleszthetők, amint azt korábban láthattuk, lehetséges, hogy objektummegosztási módszereket és tulajdonságokat hozhat létre. De a legegyszerűbbet akarjuk.

Szerencsére az ECMAScript 6 adja meg a kulcsszót class, így nagyon könnyű létrehozni egy osztályt:

class Point { constructor(x, y) { this.x = x; this.y = y; } toString() { return `(${this.x},${this.y})`; } }

Tehát véleményem szerint ez a legjobb módszer az osztályok JavaScript-ben történő deklarálására. Az osztályok gyakran kapcsolódnak az örökléshez:

class Point extends HasXY { constructor(x, y) { super(x, y); } toString() { return `(${this.x},${this.y})`; } }

Amint a fenti példában látható, egy másik osztály kibővítéséhez elegendő a kulcsszó használata extends.

Létrehozhat egy objektumot egy osztályból az newoperátor segítségével:

const p = new Point(1,1); console.log(p instanceof Point); // prints true

Az osztályok definiálásának egy jó objektumorientált módszerének a következőket kell biztosítania:

  • egyszerű szintaxis az osztály deklarálásához
  • egyszerű módja az aktuális példány elérésének, más néven this
  • egy egyszerű szintaxis az osztály kibővítésére
  • egyszerű módja a szuper osztálypéldány, más néven elérésének super
  • esetleg egy egyszerű módszer annak megállapítására, hogy egy objektum egy adott osztály példánya-e. obj instanceof AClassvissza kell térnie, trueha az objektum az adott osztály példánya.

Az új classszintaxis a fenti pontokat tartalmazza.

A classkulcsszó bevezetése előtt hogyan lehet osztályt definiálni a JavaScript-ben?

Ezenkívül mi is valójában a JavaScript osztálya? Miért beszélünk gyakran prototípusokról ?

Osztályok a JavaScript-ben 5

A Mozilla MDN oldaláról az osztályokról:

Az ECMAScript 2015-ben bevezetett JavaScript-osztályok elsősorban a JavaScript meglévő prototípus-alapú öröklődésének szintaktikai cukrát jelentik . Az osztály szintaxisa nem vezet be új objektum-orientált öröklési modellt a JavaScript-be.

A kulcsfogalom itt a prototípus-alapú öröklés . Mivel sok félreértés van arról, hogy mi is ez a fajta öröklődés, lépésről lépésre haladok, classkulcsszóról functionkulcsszóra haladva .

class Shape {} console.log(typeof Shape); // prints function

Úgy tűnik, hogy class, és functionösszefügg. Van classegy álnév function? Nem, nem az.

Shape(2); // Uncaught TypeError: Class constructor Shape cannot be invoked without 'new'

Úgy tűnik tehát, hogy azok, akik bevezették a classkulcsszót, azt akarták mondani nekünk, hogy az osztály egy olyan függvény, amelyet az newoperátor segítségével kell meghívni .

var Shape = function Shape() {} // Or just function Shape(){} var aShape = new Shape(); console.log(aShape instanceof Shape); // prints true

The example above shows that we can use function to declare a class. We cannot, however, force the user to call the function using the new operator. It is possible to throw an exception if the new operator wasn’t used to call the function.

Anyway I suggest you don’t put that check in every function that acts as a class. Instead use this convention: any function whose name begins with a capital letter is a class and must be called using the new operator.

Let’s move on, and find out what a prototype is:

class Shape { getName() { return 'Shape'; } } console.log(Shape.prototype.getName); // prints function getName() ...

Each time you declare a method inside a class, you actually add that method to the prototype of the corresponding function. The equivalent in JS 5 is:

function Shape() {} Shape.prototype.getName = function getName() { return 'Shape'; }; console.log(new Shape().getName()); // prints Shape

Sometimes the class-functions are called constructors because they act like constructors in a regular class.

You may wonder what happens if you declare a static method:

class Point { static distance(p1, p2) { // ... } } console.log(Point.distance); // prints function distance console.log(Point.prototype.distance); // prints undefined

Since static methods are in a 1 to 1 relation with classes, the static function is added to the constructor-function, not to the prototype.

Let’s recap all these concepts in a simple example:

function Point(x, y) { this.x = x; this.y = y; } Point.prototype.toString = function toString() { return '(' + this.x + ',' + this.y + ')'; }; Point.distance = function distance() { // ... } console.log(new Point(1,2).toString()); // prints (1,2) console.log(new Point(1,2) instanceof Point); // prints true

Up to now, we have found a simple way to:

  • declare a function that acts as a class
  • access the class instance using the this keyword
  • create objects that are actually an instance of that class (new Point(1,2) instanceof Point returns true )

But what about inheritance? What about accessing the super class?

class Hello { constructor(greeting) { this._greeting = greeting; } greeting() { return this._greeting; } } class World extends Hello { constructor() { super('hello'); } worldGreeting() { return super.greeting() + ' world'; } } console.log(new World().greeting()); // Prints hello console.log(new World().worldGreeting()); // Prints hello world

Above is a simple example of inheritance using ECMAScript 6, below the same example using the the so called prototype inheritance:

function Hello(greeting) { this._greeting = greeting; } Hello.prototype.greeting = function () { return this._greeting; }; function World() { Hello.call(this, 'hello'); } // Copies the super prototype World.prototype = Object.create(Hello.prototype); // Makes constructor property reference the sub class World.prototype.constructor = World; World.prototype.worldGreeting = function () { const hello = Hello.prototype.greeting.call(this); return hello + ' world'; }; console.log(new World().greeting()); // Prints hello console.log(new World().worldGreeting()); // Prints hello world

This way of declaring classes is also suggested in the Mozilla MDN example here.

Using the class syntax, we deduced that creating classes involves altering the prototype of a function. But why is that so? To answer this question we must understand what the new operator actually does.

New operator in JavaScript

The new operator is explained quite well in the Mozilla MDN page here. But I can provide you with a relatively simple example that emulates what the new operator does:

function customNew(constructor, ...args) { const obj = Object.create(constructor.prototype); const result = constructor.call(obj, ...args); return result instanceof Object ? result : obj; } function Point() {} console.log(customNew(Point) instanceof Point); // prints true

Note that the real new algorithm is more complex. The purpose of the example above is just to explain what happens when you use the new operator.

When you write new Point(1,2)what happens is:

  • The Point prototype is used to create an object.
  • The function constructor is called and the just created object is passed as the context (a.k.a. this) along with the other arguments.
  • If the constructor returns an Object, then this object is the result of the new, otherwise the object created from the prototype is the result.

So, what does prototype inheritance mean? It means that you can create objects that inherit all the properties defined in the prototype of the function that was called with the new operator.

If you think of it, in a classical language the same process happens: when you create an instance of a class, that instance can use the this keyword to access to all the functions and properties (public) defined in the class (and the ancestors). As opposite to properties, all the instances of a class will likely share the same references to the class methods, because there is no need to duplicate the method’s binary code.

Functional programming

Sometimes people say that JavaScript is not well suited for Object Oriented programming, and you should use functional programming instead.

While I don’t agree that JS is not suited for O.O.P, I do think that functional programming is a very good way of programming. In JavaScript functions are first class citizens (e.g. you can pass a function to another function) and it provides features like bind , call or apply which are base constructs used in functional programming.

In addition RX programming could be seen as an evolution (or a specialization) of functional programming. Have a look to RxJs here.

Conclusion

Use, when possible, ECMAScript 6 class syntax:

class Point { toString() { //... } }

or use function prototypes to define classes in ECMAScript 5:

function Point() {} Point.prototype.toString = function toString() { // ... }

Hope you enjoyed the reading!