Увод у објектно оријентисано програмирање у ЈаваСцрипт-у: објекти, прототипови и класе

У многим програмским језицима часови су добро дефинисан појам. У ЈаваСцрипт-у то није случај. Или бар то није био случај. Ако тражите ООП и ЈаваСцрипт, налетјет ћете на много чланака с пуно различитих рецепата о томе како можете опонашати а classу ЈаваСцрипт-у.

Постоји ли једноставан, КИСС начин за дефинисање класе у ЈаваСцрипт-у? А ако је тако, зашто толико различитих рецепата за дефинисање класе?

Пре него што одговоримо на та питања, хајде да боље разумемо шта је ЈаваСцрипт Object.

Објекти у ЈаваСцрипт-у

Почнимо са врло једноставним примером:

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

У горе наведеном исечку кода креира се објекат и побољшава својством foo. Могућност додавања ствари постојећем објекту је оно по чему се ЈаваСцрипт разликује од класичних језика попут Јаве.

Детаљније, чињеница да се објекат може побољшати омогућава стварање инстанце „имплицитне“ класе без потребе да се заправо креира класа. Појаснимо овај концепт на примеру:

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});

У горњем примеру, није ми требала класа Поинт да бих креирао тачку, већ сам само проширио инстанцу Objectдодавања xи yсвојстава. Удаљеност функције не занима да ли су аргументи инстанца класе Pointили не. Док не позовете distanceфункцију са два објекта која имају xи yсвојство типа Number, она ће радити сасвим у реду. Овај концепт се понекад назива типкање патке .

До сада сам користио само објект података: објекат који садржи само податке и нема функције. Али у ЈаваСцрипт-у је могуће додати функције објекту:

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

Овог пута објекти који представљају 2Д тачку имају toString()метод. У горњем примеру, toStringкод је дуплиран, што није добро.

Много је начина да се то дуплирање избегне, а заправо ћете у различитим чланцима о објектима и класама у ЈС наћи различита решења. Да ли сте икада чули за „Узорак модула откривања“? Садржи речи „образац“ и „откривање“, звучи кул, а „модул“ је неопходан. Дакле, то мора бити прави начин за стварање објеката ... осим што није. Откривање узорка модула у неким случајевима може бити прави избор, али дефинитивно није подразумевани начин креирања објеката са понашањем.

Сада смо спремни за увођење наставе.

Часови у ЈаваСцрипт-у

Шта је час? Из речника: класа је „скуп или категорија ствари која има неко заједничко својство или атрибут и разликује се од других по врсти, типу или квалитету“.

У програмским језицима често кажемо „Објекат је инстанца класе“. То значи да, користећи класу, могу да створим много објеката и сви они деле методе и својства.

Како се објекти могу побољшати, као што смо видели раније, постоје начини за стварање метода и својстава за дељење објеката. Али ми желимо најједноставнији.

Срећом ЕЦМАСцрипт 6 пружа кључну реч class, што олакшава стварање класе:

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

Дакле, по мом мишљењу, то је најбољи начин декларисања класа у ЈаваСцрипт-у. Часови су често повезани са наслеђивањем:

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

Као што видите у примеру горе, за проширење друге класе довољно је користити кључну реч extends.

Можете створити објекат из класе помоћу newоператора:

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

Добар објектно оријентисан начин дефинисања класа треба да обезбеди:

  • једноставна синтакса за декларацију класе
  • једноставан начин приступа тренутној инстанци, ака this
  • једноставна синтакса за проширење класе
  • једноставан начин за приступ инстанци супер класе, ака super
  • могуће, једноставан начин да се утврди да ли је објекат инстанца одређене класе. obj instanceof AClassтреба да се врати trueако је тај објекат инстанца те класе.

Нова classсинтакса даје све горе наведене тачке.

Пре увођења classкључне речи, на који начин је могао да се дефинише класа у ЈаваСцрипт-у?

Поред тога, шта је заиста класа у ЈаваСцрипт-у? Зашто често говоримо о прототиповима ?

Часови у ЈаваСцрипт-у 5

Са Мозилла МДН странице о часовима:

ЈаваСцрипт класе, уведене у ЕЦМАСцрипт 2015, првенствено су синтаксички шећер у односу на постојеће ЈаваСцрипт наслеђивање засновано на прототипу . Синтакса класе не уводи нови објектно оријентисани модел наслеђивања у ЈаваСцрипт.

Кључни концепт овде је наслеђивање засновано на прототипу . Будући да постоји пуно неспоразума око тога шта је то наследство, наставићу корак по корак, прелазећи са classкључне речи на functionкључну реч.

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

Чини се да су classи functionповезани. Да ли је classто само псеудоним function? Не, није.

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

Изгледа да су људи који су увели classкључну реч желели да нам кажу да је класа функција која се мора позвати помоћу newоператора.

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.

Иако се не слажем да ЈС није погодан за ООП, мислим да је функционално програмирање веома добар начин програмирања. У ЈаваСцрипт функцијама су грађани прве класе (нпр. Можете пренијети функцију другој функцији) и она пружа функције попут bind, callили applyкоје су основне конструкције које се користе у функционалном програмирању.

Поред тога, РКС програмирање се може посматрати као еволуција (или специјализација) функционалног програмирања. Погледајте овде РкЈ.

Закључак

Користите, када је то могуће, classсинтаксу ЕЦМАСцрипт 6 :

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

или користите прототипове функција за дефинисање класа у ЕЦМАСцрипт 5:

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

Надам се да сте уживали у читању!