Articolo originale: Object Oriented Programming in JavaScript – Explained with Examples

JavaScript non è un linguaggio orientato agli oggetti o basato su classi. Nonostante ciò ci sono dei modi per utilizzare la programmazione orientata agli oggetti (object oriented programming, OOP).

In questo tutorial, spiegherò la OOP e ti mostrerò come farne uso.

Secondo Wikipedia, la programmazione basata su classi è

uno stile di programmazione orientata agli oggetti (OOP) in cui l'ereditarietà avviene tramite la definizione di classi di oggetti, invece che tramite i soli oggetti.

Il modello più popolare di OOP è quello basato su classi.

Ma come ho menzionato, JavaScript non è un linguaggio basato su classi – è un linguaggio basato sul prototype.

Secondo la documentazione di Mozilla:

Un linguaggio basato sul prototype possiede il concetto di oggetto prototipico, un oggetto usato come stampo dal quale ottenere le proprietà iniziali per un nuovo oggetto.

Dai un'occhiata a questo codice:

let names = {
    fname: "Dillion",
    lname: "Megida"
}
console.log(names.fname);
console.log(names.hasOwnProperty("mname"));
// Output atteso
// Dillion
// false

La variabile oggetto names ha solo due proprietà – fname e lname. Nessun metodo.

Quindi da dove salta fuori hasOwnProperty?

Viene fuori dal prototype di Object.

Prova a vedere sulla console il contenuto della variabile:

console.log(names);

Espandendo il risultato sulla console otterrai questo:

1-1
console.log(names)

Nota l'ultima proprietà – __proto__ – e prova ad espanderla:

2-1
La proprietà __proto__ di names

Vedrai una serie di proprietà sotto il costruttore Object. Tutte queste proprietà derivano dal prototype globale di Object. Guardando con attenzione noterai anche la nostra proprietà nascosta hasOwnProperty.

In altre parole, tutti gli oggetti hanno accesso al prototype di Object. Non possiedono queste proprietà, ma è garantito loro l'accesso alle proprietà nel prototype.

La proprietà __proto__

Questa proprietà punta all'oggetto usato come prototype.

Si tratta di una proprietà di ogni oggetto che gli dà accesso alla proprietà prototype di Object.

Ogni oggetto ha questa proprietà come impostazione predefinita, che fa riferimento al prototype di Object, tranne se configurata diversamente (ovvero se __proto__ per quell'oggetto viene puntata su un altro prototype).

Modificare la proprietà __proto__

Questa proprietà può essere modificata definendo esplicitamente che dovrebbe far riferimento a un altro prototype. I seguenti metodi vengono usati a questo scopo:

Object.create()

function DogObject(name, age) {
    let dog = Object.create(constructorObject);
    dog.name = name;
    dog.age = age;
    return dog;
}
let constructorObject = {
    speak: function(){
        return "I am a dog"
    }
}
let bingo = DogObject("Bingo", 54);
console.log(bingo);

Ecco come si presenta la console:

3-1
console.log(bingo)

Hai notato la proprietà __proto__ e il metodo speak?

Object.create fa sì che l'argomento passatole diventi il prototype.

Parola chiave new

function DogObject(name, age) {
    this.name = name;
    this.age = age;
}
DogObject.prototype.speak = function() {
    return "I am a dog";
}
let john = new DogObject("John", 45);

La proprietà __proto__ di john è diretta verso il prototype di DogObject. Ma ricorda che il prototype di DogObject è un oggetto (coppie chiave-valore), dunque possiede anch'esso una proprietà __proto__ che fa riferimento al prototype globale di Object.

Questa tecnica viene chiamata PROTOTYPE CHAINING (concatenamento di prototype).

Nota che l'approccio con la parola chiave new fa la stessa cosa di Object.create(), ma lo rende l'operazione più semplice dato che fa alcune cose automaticamente per te.

Quindi...

Ogni oggetto in JavaScript ha accesso al prototype di Object di default. Se configurato per fare uso di un altro prototype, diciamo prototype2, allora prototype2 avrà comunque accesso al prototype di Object di default e così via.

Combinazione oggetto + funzione

Probabilmente sei confuso dal fatto che DogObject sia una funzione (function DogObject(){}) e che abbia delle proprietà accessibili con la notazione a punto. Ciò viene chiamata combinazione funzione oggetto.

Quando le funzioni vengono dichiarate, vengono date loro molte proprietà di default. Ricorda che anche le funzioni sono degli oggetti in JavaScript.

E ora, le classi

La parola chiave class è stata introdotta in JavaScript con ECMAScript 2015. Rende JavaScript simile a un linguaggio OOP. Ma si tratta solo dello zucchero sintattico sulla base della tecnica di prototyping esistente, la quale continua in background, mentre dall'esterno il tutto sembra avere l'aspetto di una OOP. Adesso cercheremo di capire come sia possibile.

Il seguente esempio è un generico utilizzo di class in JavaScript:

class Animals {
    constructor(name, specie) {
        this.name = name;
        this.specie = specie;
    }
    sing() {
        return `${this.name} can sing`;
    }
    dance() {
        return `${this.name} can dance`;
    }
}
let bingo = new Animals("Bingo", "Hairy");
console.log(bingo);

Questo è il risultato sulla console:

5-1
console.log(bingo)

__proto__ fa riferimento al prototype di Animals (che a sua volta fa riferimento al prototype di Object).

Da qui, possiamo vedere che il costruttore definisce le caratteristiche principali, mentre tutto ciò che è al di fuori del costruttore (sing() e dance()) costituisce delle funzionalità aggiuntive (prototype).

In background, usando l'approccio con la parola chiave new, quanto sopra si traduce in:

function Animals(name, specie) {
    this.name = name;
    this.specie = specie;
}
Animals.prototype.sing = function(){
    return `${this.name} can sing`;
}
Animals.prototype.dance = function() {
    return `${this.name} can dance`;
}
let Bingo = new Animals("Bingo", "Hairy");

Sottoclassi

Questa è una caratteristica della OOP, per cui una classe eredita caratteristiche dalla sua classe genitore ma possiede anche caratteristiche extra che il genitore non ha.

Ad esempio, diciamo di voler creare una classe Cats. Invece di creare la classe da zero – definendo proprietà come name, age e specie ex novo – possiamo far ereditare delle proprietà dalla classe genitore Animals.

La classe Cats può anche avere proprietà extra come wiskerColor, in questo caso.

Vediamo come ottenere una sottoclasse tramite class.

Abbiamo bisogno di un genitore da cui la sottoclasse erediterà delle caratteristiche. Esaminiamo il seguente codice:

class Animals {
    constructor(name, age) {
        this.name = name;
        this.age = age;
    }
    sing() {
        return `${this.name} can sing`;
    }
    dance() {
        return `${this.name} can dance`;
    }
} 
class Cats extends Animals {
    constructor(name, age, whiskerColor) {
        super(name, age);
        this.whiskerColor = whiskerColor;
    }
    whiskers() {
        return `I have ${this.whiskerColor} whiskers`;
    }
}
let clara = new Cats("Clara", 33, "indigo");

Con il codice qui sopra, otteniamo il seguente output:

console.log(clara.sing());
console.log(clara.whiskers());
// Output atteso
// "Clara can sing"
// "I have indigo whiskers"

Osservando il contenuto di clara nella console, vedremo:

6-1
console.log(clara)

Puoi notare che clara ha una proprietà __proto__ che fa riferimento al costruttore Cats e ha accesso al metodo whiskers(). La proprietà __proto__ ha a sua volta una proprietà __proto__ che fa riferimento al costruttore Animals avendo così accesso a sing() e dance(). name e age sono proprietà che esistono in ogni oggetto creato così.

Usando l'approccio del metodo Object.create, quanto sopra diventa:

function Animals(name, age) {
    let newAnimal = Object.create(animalConstructor);
    newAnimal.name = name;
    newAnimal.age = age;
    return newAnimal;
}
let animalConstructor = {
    sing: function() {
        return `${this.name} can sing`;
    },
    dance: function() {
        return `${this.name} can dance`;
    }
}
function Cats(name, age, whiskerColor) {
    let newCat = Animals(name, age);
    Object.setPrototypeOf(newCat, catConstructor);
    newCat.whiskerColor = whiskerColor;
    return newCat;
}
let catConstructor = {
    whiskers() {
        return `I have ${this.whiskerColor} whiskers`;
    }
}
Object.setPrototypeOf(catConstructor, animalConstructor);
const clara = Cats("Clara", 33, "purple");
clara.sing();
clara.whiskers();
// Output atteso
// "Clara can sing"
// "I have purple whiskers"

Object.setPrototypeOf è un metodo che accetta due argomenti – l'oggetto (primo argomento) e il prototype desiderato (secondo argomento).

Nel codice qui sopra, la funzione Animals restituisce un oggetto con animalConstructor come prototype. La funzione Cats restituisce un oggetto con catConstructor come prototype. catConstructor, d'altro canto, ha il prototype di animalConstructor.

Dunque, animali comuni hanno accesso solo a animalConstructor ma i gatti (cat) hanno accesso a catConstructor e animalConstructor.

In conclusione

JavaScript sfrutta la sua natura basata sul prototype per accogliere gli sviluppatori OOP nel suo ecosistema. Fornisce anche dei modi semplici per creare prototype e organizzare dati correlati.

I veri linguaggi OOP non eseguono prototyping in background – ricordalo.

Un sentito ringraziamento a Will Sentance. Tutto ciò che vedi in questo articolo (eccetto qualche cosuccia extra) l'ho imparato grazie al suo corso "JavaScript: The Hard Parts of Object Oriented JavaScript". Dovresti dargli un'occhiata.

Contattami su Twitter per domande o contributi.

Grazie per aver letto : )

Risorse utili (in inglese)