En introduktion til objektorienteret programmering i JavaScript

Denne artikel er til JavaScript-studerende, der ikke har nogen forkundskaber inden for objektorienteret programmering (OOP). Jeg fokuserer på de dele af OOP, der kun er relevante for JavaScript og ikke OOP generelt. Jeg springer polymorfisme over, fordi den passer bedre til et statisk typet sprog.

Hvorfor har du brug for at vide dette?

Har du valgt JavaScript som dit første programmeringssprog? Ønsker du at være en hot-shot-udvikler, der arbejder på gigantiske enterprise-systemer, der spænder hundrede tusind linjer kode eller mere?

Medmindre du lærer at omfavne objektorienteret programmering fuldt ud, vil du gå rigtigt vild.

Forskellige tankesæt

I fodbold kan du spille fra et sikkert forsvar, du kan spille med høje bolde fra siderne, eller du kan angribe som om der ikke er nogen i morgen. Alle disse strategier har det samme mål: At vinde spillet.

Det samme gælder for programmeringsparadigmer. Der er forskellige måder at nærme sig et problem og designe en løsning på.

Objektorienteret programmering eller OOP er paradigmet for moderne applikationsudvikling. Det understøttes af store sprog som Java, C # eller JavaScript.

Det objektorienterede paradigme

Fra et OOP-perspektiv er en applikation en samling af "objekter", der kommunikerer med hinanden. Vi baserer disse objekter på ting i den virkelige verden, som produkter i beholdning eller medarbejderregistreringer. Objekter indeholder data og udfører en vis logik baseret på deres data. Som et resultat er OOP-kode meget let at forstå. Det, der ikke er så let, er at beslutte, hvordan man i første omgang deler en applikation i disse små genstande.

Hvis du var som mig, da jeg hørte det første gang, har du ingen anelse om, hvad dette faktisk betyder - det hele lyder meget abstrakt. Det er helt fint at føle sig sådan. Det er vigtigere, at du har hørt ideen, skal du huske den og prøve at anvende OOP i din kode. Over tid vil du få erfaring og tilpasse mere af din kode med dette teoretiske koncept.

Lektion : OOP baseret på virkelige objekter lader nogen læse din kode og forstå, hvad der foregår.

Objekt som midtpunkt

Et simpelt eksempel hjælper dig med at se, hvordan JavaScript implementerer de grundlæggende principper for OOP. Overvej en shopping use case, hvor du lægger produkter i din kurv og derefter beregner den samlede pris, du skal betale. Hvis du tager din JavaScript-viden og koder brugssagen uden OOP, vil det se sådan ud:

const bread = {name: 'Bread', price: 1};const water = {name: 'Water', price: 0.25};
const basket = [];basket.push(bread);basket.push(bread);basket.push(water);basket.push(water);basket.push(water);
const total = basket .map(product => product.price) .reduce((a, b) => a + b, 0);
console.log('one has to pay in total: ' + total);

OOP-perspektivet gør det lettere at skrive bedre kode, fordi vi tænker på objekter, som vi ville støde på dem i den virkelige verden. Da vores brugssag indeholder en kurv med produkter, har vi allerede to slags objekter - kurvobjektet og produktobjekterne.

OOP-versionen af ​​indkøbssagen kunne skrives som:

const bread = new Product('bread', 1);const water = new Product('water', .25)const basket = new Basket();basket.addProduct(2, bread);basket.addProduct(3, water);basket.printShoppingInfo();

Som du kan se på første linje opretter vi et nyt objekt ved hjælp af nøgleordet newefterfulgt af navnet på det, der kaldes en klasse (beskrevet nedenfor). Dette returnerer et objekt, som vi gemmer til den variable brød. Vi gentager det for det variable vand og tager en lignende vej for at oprette en variabel kurv. Når du har føjet disse produkter til din kurv, udskriver du endelig det samlede beløb, du skal betale.

Forskellen mellem de to kodestykker er tydelig. OOP-versionen læser næsten som ægte engelske sætninger, og du kan let fortælle, hvad der foregår.

Lektion : Et objekt modelleret på virkelige ting består af data og funktioner.

Klasse som skabelon

Vi bruger klasser i OOP som skabeloner til oprettelse af objekter. Et objekt er en "forekomst af en klasse" og "instantiering" er oprettelsen af ​​et objekt baseret på en klasse. Koden er defineret i klassen, men kan ikke udføres, medmindre den er i et levende objekt.

Du kan se på klasser som tegningerne for en bil. De definerer bilens egenskaber som drejningsmoment og hestekræfter, interne funktioner såsom luft-til-brændstof-forhold og offentligt tilgængelige metoder som tænding. Det er kun når en fabrik starter bilen, men du kan dreje nøglen og køre.

I vores brugssag bruger vi produktklassen til at instantiere to objekter, brød og vand. Naturligvis har disse objekter brug for kode, som du skal give i klasser. Det går sådan her:

function Product(_name, _price) { const name = _name; const price = _price;
this.getName = function() { return name; };
this.getPrice = function() { return price; };}
function Basket() { const products = [];
this.addProduct = function(amount, product) { products.push(...Array(amount).fill(product)); };
this.calcTotal = function() { return products .map(product => product.getPrice()) .reduce((a, b) => a + b, 0); };
this.printShoppingInfo = function() { console.log('one has to pay in total: ' + this.calcTotal()); };}

En klasse i JavaScript ligner en funktion, men du bruger den forskelligt. Funktionens navn er klassens navn og har store bogstaver. Da det ikke returnerer noget, kalder vi ikke funktionen på den sædvanlige måde som const basket = Product('bread', 1);. I stedet tilføjer vi nøgleordet nyt som const basket = new Product('bread', 1);.

Koden inde i funktionen er konstruktøren. Denne kode udføres hver gang et objekt instantieres. Produktet har parametrene _nameog _price. Hvert nyt objekt gemmer disse værdier inde i det.

Desuden kan vi definere funktioner, som objektet vil levere. Vi definerer disse funktioner ved at forhåndsudgive dette nøgleord, som gør dem tilgængelige udefra (se indkapsling). Bemærk, at funktionerne har fuld adgang til egenskaberne.

Class Basket kræver ingen argumenter for at oprette et nyt objekt. Instantiering af et nyt kurvobjekt genererer simpelthen en tom liste over produkter, som programmet kan udfylde bagefter.

Lektion : En klasse er en skabelon til generering af objekter under løbetiden.

Indkapsling

Du kan støde på en anden version af, hvordan man erklærer en klasse:

function Product(name, price) { this.name = name; this.price = price;}

Vær opmærksom på tildelingen af ​​egenskaberne til variablen this. Ved første øjekast ser det ud til at være en bedre version, fordi den ikke længere kræver getter (getName & getPrice) -metoderne og er derfor kortere.

Desværre har du nu givet fuld adgang til ejendommene udefra. Så alle kunne få adgang til og ændre det:

const bread = new Product('bread', 1);bread.price = -10;

This is something you don’t want as it makes the application more difficult to maintain. What would happen if you added validation code to prevent, for example, prices less than zero? Any code that accesses the price property directly would bypass the validation. This could introduce errors that would be difficult to trace. Code that uses the object’s getter methods, on the other hand, is guaranteed to go through the object’s price validation.

Objects should have exclusive control over their data. In other words, the objects “encapsulate” their data and prevent other objects from accessing the data directly. The only way to access the data is indirect via the functions written into the objects.

Data and processing (aka logic) belong together. This is especially true when it comes to larger applications where it is very important that processing data is restricted to specifically-defined places.

Done right, OOP produces modularity by design, the holy grail in software development. It keeps away the feared spaghetti-code where everything is tightly coupled and you don’t know what happens when you change a small piece of code.

In our case, objects of class Product don’t let you change the price or the name after their initialization. The instances of Product are read-only.

Lesson: Encapsulation prevents access to data except through the object’s functions.

Inheritance

Inheritance lets you create a new class by extending an existing class with additional properties and functions. The new class “inherits” all of the features of its parent, avoiding the creation of new code from scratch. Furthermore, any changes made to the parent class will automatically be available to the child class. This makes updates much easier.

Let’s say we have a new class called Book that has a name, a price and an author. With inheritance, you can say that a Book is the same as a Product but with the additional author property. We say that Product is the superclass of Book and Book is a subclass of Product:

function Book(_name, _price, _author) { Product.call(this, _name, _price); const author = _author; this.getAuthor = function() { return author; }}

Note the additional Product.call along the this as the first argument. Please be aware: Although book provides the getter methods, it still doesn’t have direct access to the properties name and price. Book must call that data from the Product class.

You can now add a book object to the basket without any issues:

const faust = new Book('faust', 12.5, 'Goethe');basket.addProduct(1, faust);

Basket expects an object of type Product. Since book inherits from Product through Book, it is also a Product.

Lesson: Subclasses can inherit properties and functions from superclasses while adding properties and functions of their own.

JavaScript and OOP

You will find three different programming paradigms used to create JavaScript applications. They are Prototype-Based Programming, Object-Oriented Programming and Functional-Oriented Programming.

The reason for this lies in JavaScript’s history. Originally, it was prototype-based. JavaScript was not intended as a language for large applications.

Against the plan of its founders, developers increasingly used JavaScript for bigger applications. OOP was grafted on top of the original prototype-based technique.

The prototype-based approach is shown below. It is seen as the “classical and default way” to construct classes. Unfortunately it does not support encapsulation.

Even though JavaScript’s support for OOP is not at the same level as other languages like Java, it is still evolving. The release of version ES6 added a dedicated class keyword we could use. Internally, it serves the same purpose as the prototype property, but it reduces the size of the code. However, ES6 classes still lack private properties, which is why I stuck to the “old way”.

For the sake of completeness, this is how we would write the Product, Basket and Book with ES6 classes and also with the prototype (classical and default) approach. Please note that these versions don’t provide encapsulation:

// ES6 version
class Product { constructor(name, price) { this.name = name; this.price = price; }}
class Book extends Product { constructor(name, price, author) { super(name, price); this.author = author; }}
class Basket { constructor() { this.products = []; }
 addProduct(amount, product) { this.products.push(…Array(amount).fill(product)); }
 calcTotal() { return this.products .map(product => product.price) .reduce((a, b) => a + b, 0); }
 printShoppingInfo() { console.log('one has to pay in total: ' + this.calcTotal()); }}
const bread = new Product('bread', 1);const water = new Product('water', 0.25);const faust = new Book('faust', 12.5, 'Goethe');
const basket = new Basket();basket.addProduct(2, bread);basket.addProduct(3, water);basket.addProduct(1, faust);basket.printShoppingInfo();
//Prototype versionfunction Product(name, price) { this.name = name; this.price = price;}function Book(name, price, author) { Product.call(this, name, price); this.author = author;}Book.prototype = Object.create(Product.prototype);Book.prototype.constructor = Book;function Basket() { this.products = [];}Basket.prototype.addProduct = function(amount, product) { this.products.push(...Array(amount).fill(product));};Basket.prototype.calcTotal = function() { return this.products .map(product => product.price) .reduce((a, b) => a + b, 0);};Basket.prototype.printShoppingInfo = function() { console.log('one has to pay in total: ' + this.calcTotal());};

Lesson: OOP was added to JavaScript later in its development.

Summary

As a new programmer learning JavaScript, it will take time to appreciate Object-Oriented Programming fully. The important things to understand at this early stage are the principles the OOP paradigm is based on and the benefits they provide:

  • Objects modeled on real-world things are the centerpiece of any OOP-based application.
  • Encapsulation protects data from uncontrolled access.
  • Objects have functions that operate on the data the objects contain.
  • Classes are the templates used to instantiate objects.
  • Inheritance is a powerful tool for avoiding redundancy.
  • OOP is more verbose but easier to read than other coding paradigms.
  • Since OOP came later in JavaScript’s development, you may come across older code that uses prototype or functional programming techniques.

Further reading

  • //developer.mozilla.org/en-US/docs/Learn/JavaScript/Objects/Object-oriented_JS
  • //voidcanvas.com/es6-private-variables/
  • //medium.com/@rajaraodv/is-class-in-es6-the-new-bad-part-6c4e6fe1ee65
  • //developer.mozilla.org/da-DK/docs/Learn/JavaScript/Objects/Inheritance

    * //en.wikipedia.org/wiki/Object-oriented_programming

  • //en.wikipedia.org/wiki/Object-oriented_programming