Въведение в обектно-ориентираното програмиране в JavaScript: обекти, прототипи и класове

В много езици за програмиране класовете са добре дефинирана концепция. В JavaScript това не е така. Или поне не беше така. Ако търсите OOP и JavaScript, ще срещнете много статии с много различни рецепти за това как можете да емулирате a classв JavaScript.

Има ли прост, KISS начин за дефиниране на клас в JavaScript? И ако да, защо толкова много различни рецепти за определяне на клас?

Преди да отговорим на тези въпроси, нека разберем по-добре какво представлява JavaScript Object.

Обекти в JavaScript

Нека започнем с един много прост пример:

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

В горния кодов фрагмент се създава и подобрява обект със свойство foo. Възможността за добавяне на неща към съществуващ обект е това, което прави JavaScript различен от класическите езици като Java.

По-подробно, фактът, че даден обект може да бъде подобрен, дава възможност да се създаде екземпляр на „неявен“ клас, без да е необходимо действително да се създава класът. Нека изясним тази концепция с пример:

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

В горния пример нямах нужда от клас Point, за да създам точка, просто разширих екземпляр от Objectдобавяне xи yсвойства. Функцията distance не се интересува дали аргументите са екземпляр на класа Pointили не. Докато не извикате distanceфункция с два обекта, които имат xи yсвойство тип Number, тя ще работи добре. Тази концепция понякога се нарича патица .

Досега използвах само обект от данни: обект, съдържащ само данни и без функции. Но в JavaScript е възможно да се добавят функции към обект:

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

Този път обектите, представляващи 2D точка, имат toString()метод. В горния пример toStringкодът е дублиран и това не е добре.

Има много начини да се избегне това дублиране и всъщност в различни статии за обекти и класове в JS ще намерите различни решения. Чували ли сте някога за „Разкриване на модела на модула“? Той съдържа думите „модел“ и „разкриване“, звучи страхотно и „модул“ е задължителен. Така че това трябва да е правилният начин за създаване на обекти ... с изключение на това, че не е така. Разкриването на модулен шаблон може да бъде правилният избор в някои случаи, но определено не е основният начин за създаване на обекти с поведение.

Вече сме готови да въведем класове.

Класове в JavaScript

Какво е клас? От речник: клас е „набор или категория неща, имащи някакво общо свойство или атрибут и разграничени от другите по вид, тип или качество“.

В езиците за програмиране често казваме „Обектът е екземпляр на клас“. Това означава, че с помощта на клас мога да създавам много обекти и всички те споделят методи и свойства.

Тъй като обектите могат да бъдат подобрени, както видяхме по-рано, има начини за създаване на методи и свойства за споделяне на обекти. Но ние искаме най-простия.

За щастие ECMAScript 6 предоставя ключовата дума class, което улеснява създаването на клас:

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

Така че, по мое мнение, това е най-добрият начин за деклариране на класове в JavaScript. Класовете често са свързани с наследяването:

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ключовата дума какъв беше начинът да се дефинира клас в JavaScript?

В допълнение, какво всъщност е клас в JavaScript? Защо често говорим за прототипи ?

Класове по JavaScript 5

От страницата Mozilla MDN за класовете:

JavaScript класовете, въведени в ECMAScript 2015, са предимно синтактична захар над съществуващото наследство, базирано на прототип на JavaScript . Синтаксисът на класа не въвежда нов обектно-ориентиран модел за наследяване в JavaScript.

Ключовата концепция тук е наследяване, базирано на прототип . Тъй като има много недоразумения относно това какъв вид наследство е, ще продължа стъпка по стъпка, преминавайки от 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.

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!