Eine Einführung in die objektorientierte Programmierung in JavaScript: Objekte, Prototypen und Klassen

In vielen Programmiersprachen sind Klassen ein genau definiertes Konzept. In JavaScript ist das nicht der Fall. Zumindest war das nicht der Fall. Wenn Sie nach OOP und JavaScript suchen, werden Sie auf viele Artikel mit vielen verschiedenen Rezepten stoßen, wie Sie eine classin JavaScript emulieren können .

Gibt es eine einfache KISS-Möglichkeit, eine Klasse in JavaScript zu definieren? Und wenn ja, warum so viele verschiedene Rezepte, um eine Klasse zu definieren?

Bevor wir diese Fragen beantworten, sollten wir besser verstehen, was ein JavaScript Objectist.

Objekte in JavaScript

Beginnen wir mit einem sehr einfachen Beispiel:

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

Im obigen Codeausschnitt wird ein Objekt erstellt und mit einer Eigenschaft erweitert foo. Die Möglichkeit, einem vorhandenen Objekt Dinge hinzuzufügen, unterscheidet JavaScript von klassischen Sprachen wie Java.

Genauer gesagt ermöglicht die Tatsache, dass ein Objekt erweitert werden kann, das Erstellen einer Instanz einer "impliziten" Klasse, ohne dass die Klasse tatsächlich erstellt werden muss. Lassen Sie uns dieses Konzept anhand eines Beispiels verdeutlichen:

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

Im obigen Beispiel brauchte ich keine Point-Klasse, um einen Punkt zu erstellen. Ich habe lediglich eine Instanz des ObjectHinzufügens xund der yEigenschaften erweitert. Der Funktionsentfernung ist es egal, ob die Argumente eine Instanz der Klasse sind Pointoder nicht. Bis Sie die distanceFunktion mit zwei Objekten aufrufen , die eine xund y-Eigenschaft vom Typ haben Number, funktioniert sie einwandfrei. Dieses Konzept wird manchmal als Ententypisierung bezeichnet .

Bisher habe ich nur ein Datenobjekt verwendet: ein Objekt, das nur Daten und keine Funktionen enthält. In JavaScript ist es jedoch möglich, einem Objekt Funktionen hinzuzufügen:

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

Diesmal haben die Objekte, die einen 2D-Punkt darstellen, eine toString()Methode. Im obigen Beispiel wurde der toStringCode dupliziert, und dies ist nicht gut.

Es gibt viele Möglichkeiten, diese Duplizierung zu vermeiden, und tatsächlich finden Sie in verschiedenen Artikeln über Objekte und Klassen in JS unterschiedliche Lösungen. Haben Sie jemals von dem „Revealing Module Pattern“ gehört? Es enthält die Wörter "Muster" und "enthüllen", klingt cool und "Modul" ist ein Muss. Es muss also der richtige Weg sein, Objekte zu erstellen… außer dass dies nicht der Fall ist. Das Aufdecken von Modulmustern kann in einigen Fällen die richtige Wahl sein, ist jedoch definitiv nicht die Standardmethode zum Erstellen von Objekten mit Verhalten.

Wir sind jetzt bereit, Klassen einzuführen.

Klassen in JavaScript

Was ist eine Klasse? Aus einem Wörterbuch: Eine Klasse ist „eine Menge oder Kategorie von Dingen, die eine Eigenschaft oder ein Attribut gemeinsam haben und sich von anderen nach Art, Typ oder Qualität unterscheiden.“

In Programmiersprachen sagen wir oft "Ein Objekt ist eine Instanz einer Klasse". Dies bedeutet, dass ich mit einer Klasse viele Objekte erstellen kann, die alle Methoden und Eigenschaften gemeinsam haben.

Da Objekte verbessert werden können, wie wir bereits gesehen haben, gibt es möglicherweise Möglichkeiten, Objekte mit gemeinsamen Methoden und Eigenschaften zu erstellen. Aber wir wollen das einfachste.

Glücklicherweise enthält ECMAScript 6 das Schlüsselwort class, wodurch das Erstellen einer Klasse sehr einfach ist:

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

Meiner Meinung nach ist dies der beste Weg, Klassen in JavaScript zu deklarieren. Klassen beziehen sich oft auf Vererbung:

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

Wie Sie im obigen Beispiel sehen können, reicht es aus, das Schlüsselwort zu verwenden, um eine andere Klasse zu erweitern extends.

Sie können ein Objekt aus einer Klasse mit dem newOperator erstellen :

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

Eine gute objektorientierte Methode zum Definieren von Klassen sollte Folgendes bieten:

  • Eine einfache Syntax zum Deklarieren einer Klasse
  • eine einfache Möglichkeit, auf die aktuelle Instanz zuzugreifen, auch bekannt als this
  • Eine einfache Syntax zum Erweitern einer Klasse
  • eine einfache Möglichkeit, auf die Superklasseninstanz zuzugreifen, auch bekannt als super
  • Möglicherweise eine einfache Methode, um festzustellen, ob ein Objekt eine Instanz einer bestimmten Klasse ist. obj instanceof AClasssollte zurückgeben, truewenn dieses Objekt eine Instanz dieser Klasse ist.

Die neue classSyntax bietet alle oben genannten Punkte.

classWie wurde vor der Einführung des Schlüsselworts eine Klasse in JavaScript definiert?

Was ist eigentlich eine Klasse in JavaScript? Warum sprechen wir oft über Prototypen ?

Klassen in JavaScript 5

Von der Mozilla MDN-Seite über Klassen:

JavaScript-Klassen, die in ECMAScript 2015 eingeführt wurden, sind in erster Linie syntaktischer Zucker gegenüber der vorhandenen prototypbasierten Vererbung von JavaScript . Die Klassensyntax führt kein neues objektorientiertes Vererbungsmodell in JavaScript ein.

Das Schlüsselkonzept hierbei ist die prototypbasierte Vererbung . Da es viele Missverständnisse darüber gibt, was diese Art der Vererbung ist, werde ich Schritt für Schritt von classSchlüsselwort zu functionSchlüsselwort wechseln.

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

Es scheint das classund functionsind verwandt. Ist classnur ein Alias ​​für function? Nein, ist es nicht.

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

Es scheint also, dass die Leute, die das classSchlüsselwort eingeführt haben , uns sagen wollten, dass eine Klasse eine Funktion ist, die mit dem newOperator aufgerufen werden muss .

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!