En intro til objektorientert programmering i JavaScript: objekter, prototyper og klasser

På mange programmeringsspråk er klasser et veldefinert konsept. I JavaScript er det ikke tilfelle. Eller i det minste var det ikke tilfelle. Hvis du søker etter OOP og JavaScript, vil du støte på mange artikler med mange forskjellige oppskrifter på hvordan du kan etterligne en classi JavaScript.

Er det en enkel, KISS måte å definere en klasse i JavaScript på? Og i så fall, hvorfor så mange forskjellige oppskrifter for å definere en klasse?

Før vi svarer på disse spørsmålene, la oss forstå bedre hva JavaScript Objecter.

Objekter i JavaScript

La oss begynne med et veldig enkelt eksempel:

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

I kodebiten ovenfor blir et objekt opprettet og forbedret med en eiendom foo. Muligheten for å legge til ting til et eksisterende objekt er det som gjør JavaScript forskjellig fra klassiske språk som Java.

Mer detaljert gjør det at et objekt kan forbedres det mulig å lage en forekomst av en "implisitt" klasse uten behov for å faktisk opprette klassen. La oss avklare dette konseptet 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 trengte jeg ikke en Point-klasse for å opprette et punkt, jeg utvidet bare en forekomst av å Objectlegge til xog yegenskaper. Funksjonsavstanden bryr seg ikke om argumentene er en forekomst av klassen Pointeller ikke. Inntil du kaller distancefunksjon med to objekter som har en xog yegenskap av typen Number, vil den fungere helt fint. Dette konseptet kalles noen ganger andetyping .

Inntil nå har jeg bare brukt et dataobjekt: et objekt som bare inneholder data og ingen funksjoner. Men i JavaScript er det mulig å legge til funksjoner 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 gangen har objektene som representerer et 2D-punkt en toString()metode. I eksemplet ovenfor er toStringkoden duplisert, og dette er ikke bra.

Det er mange måter å unngå duplisering, og faktisk, i forskjellige artikler om objekter og klasser i JS, vil du finne forskjellige løsninger. Har du noen gang hørt om "avslørende modulmønster"? Den inneholder ordene "mønster" og "avslørende", høres kult ut, og "modul" er et must. Så det må være den rette måten å lage objekter på ... bortsett fra at det ikke er det. Å avsløre modulmønster kan i noen tilfeller være det riktige valget, men det er definitivt ikke standard måten å lage objekter med atferd på.

Vi er nå klare til å introdusere klasser.

Kurs i JavaScript

Hva er en klasse? Fra en ordbok: en klasse er "et sett eller en kategori av ting som har en eller annen egenskap eller attributt som er felles og er forskjellig fra andre etter slag, type eller kvalitet."

I programmeringsspråk sier vi ofte "Et objekt er en forekomst av en klasse". Dette betyr at jeg ved hjelp av en klasse kan lage mange objekter, og de deler alle metoder og egenskaper.

Siden objekter kan forbedres, som vi har sett tidligere, kan det finnes måter å lage metoder og egenskaper for deling av objekter på. Men vi vil ha den enkleste.

Heldigvis gir ECMAScript 6 nøkkelordet class, noe som gjør det veldig enkelt å lage en klasse:

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

Så etter min mening er det den beste måten å erklære klasser i JavaScript. Klasser er ofte relatert 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 å bruke nøkkelordet for å utvide en annen klasse extends.

Du kan opprette et objekt fra en klasse ved hjelp av newoperatøren:

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

En god objektorientert måte å definere klasser på bør gi:

  • en enkel syntaks for å erklære en klasse
  • en enkel måte å få tilgang til nåværende forekomst, aka this
  • en enkel syntaks for å utvide en klasse
  • en enkel måte å få tilgang til superklasse-forekomsten, aka super
  • muligens en enkel måte å fortelle om et objekt er en forekomst av en bestemt klasse. obj instanceof AClassskal returnere truehvis objektet er en forekomst av den klassen.

Den nye classsyntaksen gir alle punktene ovenfor.

classHva var måten å definere en klasse i JavaScript før introduksjonen av nøkkelordet?

I tillegg, hva er egentlig en klasse i JavaScript? Hvorfor snakker vi ofte om prototyper ?

Kurs i JavaScript 5

Fra Mozilla MDN-siden om klasser:

JavaScript-klasser, introdusert i ECMAScript 2015, er primært syntaktisk sukker i forhold til JavaScript's eksisterende prototypebaserte arv . Klassesyntaks introduserer ikke en ny objektorientert arvsmodell til JavaScript.

Nøkkelkonseptet her er prototypebasert arv . Siden det er mye misforståelse om hva den slags arv er, vil jeg gå trinn for trinn og gå fra classnøkkelord til functionnøkkelord.

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

Det virker som classog functioner beslektet. Er det classbare et alias for function? Nei, det er det ikke.

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

Så det ser ut til at folkene som introduserte classnøkkelord ønsket å fortelle oss at en klasse er en funksjon som må kalles ved hjelp av 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 bruke functiontil å erklære en klasse. Vi kan imidlertid ikke tvinge brukeren til å ringe til funksjonen ved hjelp av newoperatøren. Det er mulig å kaste et unntak hvis newoperatøren ikke var vant til å ringe funksjonen.

Uansett foreslår jeg at du ikke legger den sjekken i hver funksjon som fungerer som en klasse. Bruk i stedet denne konvensjonen: enhver funksjon hvis navn begynner med en stor bokstav er en klasse og må kalles ved hjelp av newoperatøren.

La oss gå videre, og finne ut hva en prototype er:

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

Hver gang du erklærer en metode i en klasse, legger du faktisk til denne metoden i prototypen til den tilsvarende funksjonen. Tilsvarende 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.

Selv om jeg ikke er enig i at JS ikke er egnet for OOP, tror jeg at funksjonell programmering er en veldig god måte å programmere på. I JavaScript er funksjoner førsteklasses borgere (for eksempel kan du overføre en funksjon til en annen funksjon) og den inneholder funksjoner som bind, calleller applysom er basiskonstruksjoner som brukes i funksjonell programmering.

I tillegg kan RX-programmering sees på som en utvikling (eller en spesialisering) av funksjonell programmering. Ta en titt på RxJs her.

Konklusjon

Bruk, når mulig, ECMAScript 6- classsyntaksen:

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

eller bruk funksjonsprototyper for å definere klasser i ECMAScript 5:

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

Håper du likte lesingen!