Uvod u objektno orijentirano programiranje u JavaScript-u: objekti, prototipovi i klase

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 classu 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 Objectdodavanja xi ysvojstava. Udaljenost funkcije ne zanima jesu li argumenti instanca klase Pointili ne. Dok ne pozovete distancefunkciju s dva objekta koja imaju svojstvo xi i ysvojstvo 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 toStringkod 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 newoperatora:

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 AClasstrebao bi se vratiti trueako je taj objekt instanca te klase.

Nova classsintaksa pruža sve gornje točke.

Prije uvođenja classključ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 classključne riječi na functionključnu riječ.

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

Čini se da su classi functionpovezani. Je li to classsamo alias function? Ne, nije.

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

Izgleda da su nam ljudi koji su uveli classključnu riječ htjeli reći da je klasa funkcija koja se mora pozvati pomoću newoperatora.

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 functionza deklariranje klase. Međutim, ne možemo prisiliti korisnika da poziva funkciju pomoću newoperatora. Moguće je izuzeti ako se newoperator 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 newoperatora.

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 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!