
U mnogim programskim jezicima nastava je dobro definiran pojam. U JavaScriptu to nije slučaj. Ili barem to nije bio slučaj. Ako tražite OOP i JavaScript, naletjet ćete na mnogo članaka s puno različitih recepata o tome kako možete oponašati a class
u JavaScript-u.
Postoji li jednostavan, KISS način definiranja klase u JavaScript-u? I ako da, zašto toliko različitih recepata za definiranje klase?
Prije odgovora na ta pitanja, shvatimo bolje što je JavaScript Object
.
Objekti u JavaScript-u
Počnimo s vrlo jednostavnim primjerom:
const a = {}; a.foo = 'bar';
U gornjem isječku koda objekt je stvoren i poboljšan svojstvom foo
. Mogućnost dodavanja stvari postojećem objektu ono je po čemu se JavaScript razlikuje od klasičnih jezika poput Jave.
Detaljnije, činjenica da se objekt može poboljšati omogućuje stvaranje instance "implicitne" klase bez potrebe za stvarno stvaranjem klase. Pojasnimo ovaj koncept na primjeru:
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});
U gornjem primjeru, nije mi trebala klasa Point za stvaranje točke, samo sam proširio instancu Object
dodavanja x
i y
svojstava. Udaljenost funkcije ne zanima jesu li argumenti instanca klase Point
ili ne. Dok ne pozovete distance
funkciju s dva objekta koja imaju svojstvo x
i i y
svojstvo tipa Number
, ona će raditi sasvim u redu. Taj se koncept ponekad naziva i tipkanje patki .
Do sada sam koristio samo podatkovni objekt: objekt koji sadrži samo podatke i nema funkcije. Ali u JavaScriptu je moguće dodati funkcije objektu:
const point1 = { x: 1, y: 1, toString() { return `(${this.x},${this.y})`; } }; const point2 = { x: 2, y: 2, toString() { return `(${this.x},${this.y})`; } };
Ovaj put objekti koji predstavljaju 2D točku imaju toString()
metodu. U gornjem primjeru toString
kod je dupliciran, a to nije dobro.
Mnogo je načina da se izbjegne dupliciranje, a zapravo ćete u različitim člancima o objektima i klasama u JS pronaći različita rješenja. Jeste li ikada čuli za "Uzorak modula otkrivanja"? Sadrži riječi "obrazac" i "otkrivanje", zvuči cool, a "modul" je neophodan. Dakle, to mora biti pravi način za stvaranje predmeta ... osim što nije. Otkrivanje uzorka modula u nekim slučajevima može biti pravi izbor, ali definitivno nije zadani način stvaranja objekata s ponašanjem.
Sada smo spremni za uvođenje nastave.
Predavanja u JavaScript-u
Što je razred? Iz rječnika: razred je „skup ili kategorija stvari koja ima neko zajedničko svojstvo ili atribut i razlikuje se od drugih po vrsti, vrsti ili kvaliteti“.
U programskim jezicima često kažemo "Objekt je instanca klase". To znači da, koristeći klasu, mogu stvoriti mnogo objekata i svi oni dijele metode i svojstva.
Budući da se objekti mogu poboljšati, kao što smo vidjeli ranije, postoje načini za stvaranje metoda i svojstava za dijeljenje objekata. Ali mi želimo onaj najjednostavniji.
Srećom ECMAScript 6 pruža ključnu riječ class
, što olakšava stvaranje klase:
class Point { constructor(x, y) { this.x = x; this.y = y; } toString() { return `(${this.x},${this.y})`; } }
Dakle, po mom mišljenju, to je najbolji način deklariranja klasa u JavaScript-u. Razredi su često povezani sa nasljeđivanjem:
class Point extends HasXY { constructor(x, y) { super(x, y); } toString() { return `(${this.x},${this.y})`; } }
Kao što možete vidjeti u gornjem primjeru, za proširenje druge klase dovoljno je koristiti ključnu riječ extends
.
Objekt možete stvoriti iz klase pomoću new
operatora:
const p = new Point(1,1); console.log(p instanceof Point); // prints true
Dobar objektno orijentirani način definiranja klasa trebao bi pružiti:
- jednostavna sintaksa za deklariranje klase
- jednostavan način pristupa trenutnoj instanci, aka
this
- jednostavna sintaksa za proširenje klase
- jednostavan način za pristup instanci super klase, aka
super
- moguće, jednostavan način da se utvrdi je li objekt primjer određene klase.
obj instanceof AClass
trebao bi se vratititrue
ako je taj objekt instanca te klase.
Nova class
sintaksa pruža sve gornje točke.
Prije uvođenja class
ključne riječi, koji je bio način definiranja klase u JavaScript-u?
Uz to, što je zapravo klasa u JavaScript-u? Zašto često govorimo o prototipovima ?
Predavanja u JavaScript 5
Sa stranice Mozilla MDN o predavanjima:
JavaScript klase, uvedene u ECMAScript 2015, prvenstveno su sintaksički šećer nad postojećim JavaScript nasljeđivanjem zasnovanim na JavaScriptu . Sintaksa klase ne uvodi novi objektno orijentirani model nasljeđivanja u JavaScript.Ključni koncept ovdje je nasljeđivanje zasnovano na prototipu . Budući da postoji puno nesporazuma oko toga kakvo je nasljeđivanje, nastavit ću korak po korak, prelazeći s class
ključne riječi na function
ključnu riječ.
class Shape {} console.log(typeof Shape); // prints function
Čini se da su class
i function
povezani. Je li to class
samo alias function
? Ne, nije.
Shape(2); // Uncaught TypeError: Class constructor Shape cannot be invoked without 'new'
Izgleda da su nam ljudi koji su uveli class
ključnu riječ htjeli reći da je klasa funkcija koja se mora pozvati pomoću new
operatora.
var Shape = function Shape() {} // Or just function Shape(){} var aShape = new Shape(); console.log(aShape instanceof Shape); // prints true
Gornji primjer pokazuje da možemo koristiti function
za deklariranje klase. Međutim, ne možemo prisiliti korisnika da poziva funkciju pomoću new
operatora. Moguće je izuzeti ako se new
operator nije koristio za pozivanje funkcije.
U svakom slučaju predlažem da ne stavljate tu provjeru u svaku funkciju koja djeluje kao klasa. Umjesto toga koristite ovu konvenciju: svaka funkcija čije ime započinje velikim slovom je klasa i mora se pozvati pomoću new
operatora.
Krenimo dalje i saznajmo što je prototip :
class Shape { getName() { return 'Shape'; } } console.log(Shape.prototype.getName); // prints function getName() ...
Svaki put kad deklarirate metodu unutar klase, zapravo je dodate u prototip odgovarajuće funkcije. Ekvivalent u JS 5 je:
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
returnstrue
)
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!