En introduktion til objektorienteret programmering i JavaScript: objekter, prototyper og klasser

På mange programmeringssprog er klasser et veldefineret koncept. I JavaScript er det ikke tilfældet. Eller i det mindste var det ikke tilfældet. Hvis du søger efter OOP og JavaScript, vil du løbe ind i mange artikler med mange forskellige opskrifter på, hvordan du kan efterligne en classi JavaScript.

Er der en enkel, KISS måde at definere en klasse i JavaScript på? Og hvis ja, hvorfor så mange forskellige opskrifter til at definere en klasse?

Før vi besvarer disse spørgsmål, lad os forstå bedre, hvad et JavaScript Objecter.

Objekter i JavaScript

Lad os begynde med et meget simpelt eksempel:

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

I ovenstående kodestykke oprettes et objekt og forbedres med en ejendom foo. Muligheden for at tilføje ting til et eksisterende objekt er, hvad der adskiller JavaScript fra klassiske sprog som Java.

Mere detaljeret gør det faktum, at et objekt kan forbedres, det muligt at oprette en forekomst af en "implicit" klasse uden behov for faktisk at oprette klassen. Lad os præcisere dette koncept med et eksempel:

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

I eksemplet ovenfor havde jeg ikke brug for en Point-klasse for at oprette et punkt, jeg udvidede bare en forekomst af Objecttilføjelse xog yegenskaber. Funktionsafstanden er ligeglad med, om argumenterne er en forekomst af klassen Pointeller ej. Indtil du kalder distancefunktion med to objekter, der har en xog yegenskab af typen Number, fungerer den fint. Dette koncept kaldes undertiden duck typing .

Indtil nu har jeg kun brugt et dataobjekt: et objekt, der kun indeholder data og ingen funktioner. Men i JavaScript er det muligt at tilføje funktioner til et objekt:

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

Denne gang har objekterne, der repræsenterer et 2D-punkt, en toString()metode. I eksemplet ovenfor er toStringkoden duplikeret, og det er ikke godt.

Der er mange måder at undgå duplikering på, og i forskellige artikler om objekter og klasser i JS finder du faktisk forskellige løsninger. Har du nogensinde hørt om det "afslørende modulmønster"? Den indeholder ordene "mønster" og "afslørende", lyder cool, og "modul" er et must. Så det må være den rigtige måde at oprette objekter på ... bortset fra at det ikke er det. At afsløre modulmønster kan i nogle tilfælde være det rigtige valg, men det er bestemt ikke standardmetoden til at skabe objekter med adfærd.

Vi er nu klar til at introducere klasser.

Klasser i JavaScript

Hvad er en klasse? Fra en ordbog: en klasse er "et sæt eller en kategori af ting, der har en ejendom eller attribut til fælles og adskiller sig fra andre efter art, type eller kvalitet."

På programmeringssprog siger vi ofte ”Et objekt er en forekomst af en klasse”. Dette betyder, at jeg ved hjælp af en klasse kan oprette mange objekter, og de deler alle metoder og egenskaber.

Da objekter kan forbedres, som vi har set tidligere, er der måder at oprette objekter, der deler metoder og egenskaber. Men vi vil have den enkleste.

Heldigvis leverer ECMAScript 6 nøgleordet class, hvilket gør det meget let at oprette en klasse:

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

Så efter min mening er det den bedste måde at erklære klasser i JavaScript på. Klasser er ofte relateret til arv:

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

Som du kan se i eksemplet ovenfor, er det nok at bruge nøgleordet for at udvide en anden klasse extends.

Du kan oprette et objekt fra en klasse ved hjælp af newoperatoren:

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

En god objektorienteret måde at definere klasser på skal give:

  • en simpel syntaks til at erklære en klasse
  • en enkel måde at få adgang til den aktuelle forekomst, aka this
  • en simpel syntaks til at udvide en klasse
  • en enkel måde at få adgang til superklasse-forekomsten, aka super
  • muligvis en enkel måde at fortælle om et objekt er en forekomst af en bestemt klasse. obj instanceof AClassskal vende tilbage, truehvis objektet er en forekomst af denne klasse.

Den nye classsyntaks indeholder alle ovenstående punkter.

classHvad var måden at definere en klasse i JavaScript før introduktion af nøgleordet?

Derudover, hvad er egentlig en klasse i JavaScript? Hvorfor taler vi ofte om prototyper ?

Klasser i JavaScript 5

Fra Mozilla MDN-siden om klasser:

JavaScript-klasser, introduceret i ECMAScript 2015, er primært syntaktisk sukker i forhold til JavaScript's eksisterende prototype-baserede arv . Klassesyntaxen introducerer ikke en ny objektorienteret arvemodel til JavaScript.

Nøglekonceptet her er prototype-baseret arv . Da der er meget misforståelse om, hvad den slags arv er, vil jeg fortsætte trin for trin og flytte fra classnøgleord til functionnøgleord.

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

Det ser ud til, at classog functioner beslægtede. Er der classbare et alias til function? Nej, det er det ikke.

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

Så det ser ud til, at de mennesker, der introducerede classnøgleord, ønskede at fortælle os, at en klasse er en funktion, der skal kaldes ved hjælp af newoperatøren.

var Shape = function Shape() {} // Or just function Shape(){} var aShape = new Shape(); console.log(aShape instanceof Shape); // prints true

Eksemplet ovenfor viser, at vi kan bruge functiontil at erklære en klasse. Vi kan dog ikke tvinge brugeren til at ringe til funktionen ved hjælp af newoperatøren. Det er muligt at kaste en undtagelse, hvis newoperatøren ikke var vant til at ringe til funktionen.

Alligevel foreslår jeg, at du ikke lægger denne check i hver funktion, der fungerer som en klasse. Brug i stedet denne konvention: enhver funktion, hvis navn begynder med et stort bogstav, er en klasse og skal kaldes ved hjælp af newoperatøren.

Lad os gå videre og finde ud af, hvad en prototype er:

class Shape { getName() { return 'Shape'; } } console.log(Shape.prototype.getName); // prints function getName() ...

Hver gang du erklærer en metode inde i en klasse, tilføjer du faktisk metoden til prototypen for den tilsvarende funktion. Ækvivalenten i JS 5 er:

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.

Selvom jeg ikke er enig i, at JS ikke er egnet til OOP, tror jeg, at funktionel programmering er en meget god måde at programmere på. I JavaScript er funktioner førsteklasses borgere (du kan f.eks. Videregive en funktion til en anden funktion), og den indeholder funktioner som bind, calleller applysom er basiskonstruktioner, der bruges i funktionel programmering.

Derudover kunne RX-programmering ses som en udvikling (eller en specialisering) af funktionel programmering. Se på RxJs her.

Konklusion

Brug, når det er muligt, ECMAScript 6 classsyntaks:

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

eller brug funktionsprototyper til at definere klasser i ECMAScript 5:

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

Håber du nød læsningen!