JavaScript Advanced

Corso completo Web Developer - Parte 2 - 2.2

Introduzione

In questa lezione scopriremo come portare il nostro codice JavaScript al prossimo livello. Parleremo di classi JS e come utilizzarle, come gestire errori nella nostra applicazione, le funzioni sincrone ed asincrone, APIs ed altro ancora. Allacciati le cinture, si vola al prossimo livello! 🛫

Considerato che questo articolo ti introduce a più concetti avanzati, ti consiglio di prenderti il tempo di studiare e praticare ogni sezione. Magari dividi lo studio di questo articolo in più giorni, in modo da poter assimilare meglio ogni sezione.

JavaScript Async - Await

Iniziamo subito con un concetto molto importante: la spiegazione di sincrono ed asincrono in JavaScript!

Call Stack

Che cos'è un programma? Un insieme di files con tante righe di codice che vengono eseguite e che queste tutte insieme costituiscono il nostro programma, sito web, applicazione mobile o qualsiasi cosa che stiamo sviluppando.

In che modo vengono eseguite queste righe di codice? In ordine, una dopo l'altra o detto anche in modo sincrono, una riga di codice alla volta.

Come fa JavaScript a sapere a che punto del programma siamo? Utilizza una cosa chiamata Call Stack. La Call Stack è una struttura dati che tiene traccia di tutte le funzioni che sono state chiamate e che sono ancora in esecuzione. Quando una funzione viene chiamata, viene aggiunta alla Call Stack. Quando la funzione è stata eseguita, viene rimossa dalla Call Stack.

Immaginiamo come se ci fosse una colonna di oggetti, uno sopra l'altro. Ogni oggetto rappresenta una funzione che è stata chiamata. Quando una funzione viene chiamata, viene aggiunta alla Call Stack. Quando la funzione è stata eseguita, viene rimossa dalla Call Stack. Questo avviene in modo "last in - first out" (LIFO), ovvero l'ultimo oggetto che viene aggiunto alla Call Stack è il primo ad essere rimosso.

Facciamo un esempio pratico con del codice JavaScript che controlla se un triangolo è corretto o meno:

const multiply = (a, b) => a + b;

const square = (a) => multiply(a, a);

const isTriangle = (a, b, c) => {
  return square(a) + square(b) === square(c);
}

isTriangle(3, 4, 5);

In isTriangle l'argomento a sarà uguale al numero 3, b sarà uguale al numero 4 e c sarà uguale al numero 5.

Cerchiamo di capire il Call Stack con questo esempio.

Quando la funzione isTriangle viene chiamata viene aggiunta alla base di questa colonna Call Stack. All'interno di questa funzione abbiamo tre chiamate alla stessa funzione square che calcola il quadrato di un numero. Quando la funzione square viene chiamata, viene aggiunta alla Call Stack e questa a sua volta chiamerà la funzione multiply che moltiplica un numero per se stesso. Anche multiply quando viene chiamata viene aggiunta alla Call Stack.

Quindi la Call Stack è così:

isTriangle
square
multiply

Per semplificare il concetto pensiamo ad un panino dove aggiungiamo un condimento sopra l'altro. Il primo condimento che aggiungiamo è isTriangle perchè è il primo ad essere chiamato. Il secondo condimento che aggiungiamo è square ed il terzo è multiply. Quindi isTriangle è alla base, square è sopra a isTriangle e multiply è sopra a square.

multiply
square
isTriangle

Quando la funzione multiply viene eseguita, torna il risultato a square e dopodichè viene rimossa dalla Call Stack. Quindi la Call Stack diventa così:

square
isTriangle

Quando la funzione square viene eseguita, torna il risultato a isTriangle e dopodichè viene rimossa dalla Call Stack. Quindi la Call Stack diventa così:

isTriangle

Ora isTriangle procede con la prossima chiamata square passando l'argomento b, poi farà lo stesso con l'argomento c.

Una volta che isTriangle avrà finito con tutte le sue chiamate interne tornerà il risultato finale a chi l'ha chiamata e poi verrà rimossa dalla Call Stack.

Single Threaded

Emanuele, ma se la promessa di gorgaTech è quella di semplificare i concetti, perché mi stai complicando la vita con questa Call Stack e adesso cosa vuoi che capisca di Single Threaded?

Hai ragione, non voglio complicarti la vita ma è importante che tu capisca bene le fondamenta. Se hai delle fondamenta solide tutto il resto sarà mooolto più facile da capire e da applicare. Non ti preoccupare, non ti chiederò di leggerti un libro della programmazione lungo 500 pagine, ma ti farò capire concetti in questo articolo che vengono spiegati in un libro di programmazione lungo 500 pagine. 🤓

Avrai sentito parlare che gli uomini non sono bravi a fare multi tasking e che devono concentrarsi su un'attività alla volta? Ecco, JavaScript è lo stesso. JavaScript è un linguaggio Single Threaded, ovvero può eseguire un solo pezzo di codice alla volta. Non può eseguire due o più pezzi di codice contemporaneamente.

JavaScript esegue una linea di codice alla volta mentre esistono alcuni linguaggi di programmazione che possono eseguire più linee di codice nello stesso momento, ad esempio Ruby, Java, Python. Questo non rende JavaScript meno potente, anzi, lo rende JavaScript più semplice da capire e da utilizzare.

Facciamo un esempio pratico. Supponiamo di accedere al sito di Facebook il quale ogni volta che lo apriamo deve scaricare un set ti dati da un server. Il sito deve scaricare questi dati e poi mostrarli all'utente. Fino a quando tutti i dati non sono stati scaricati, il sito web rimarrà bloccato sulla pagina di caricamento e non può continuare con la prossima azione.

Questa è la natura di JavaScript ma in realtà sono state sviluppate soluzioni per aggirare questo "problema".

Vediamo un esempio pratico con il metodo setTimeout che ci permette di eseguire una funzione dopo un certo intervallo di tempo.

console.log('I am first');

setTimeout(() => {
  console.log('I am second');
}, 3000);

console.log('I am third');

Abbiamo detto che JavaScript è un linguaggio Single Threaded e che esegue una linea dopo l'altra in sequenza. Prima di eseguire una linea aspetta che la linea precedente sia stata eseguita. Sulla base di questo concetto il risultato dell'esempio dovrebbe essere:

I am first
I am second
I am third

In realtà il risultato sarà:

I am first
I am third
I am second

Provalo negli strumenti per sviluppatori del tuo browser!

Perché? Perché il metodo setTimeout nota che abbiamo passato un argomento 3000 millisecondi (3 secondi) come Callback, quindi passerà il suo contenuto al Browser e gli chiederà "Hey Browser, esegui questa funzione fra 3 secondi, io nel frattempo continuo con il resto del codice". Il Browser accetta la richiesta e esegue la funzione Callback dopo 3 secondi.

In che modo avviene tutto questo? JavaScript sfrutta il concetto di "Web APIs" che i Browsers ci mettono a disposizione. Le Web APIs sono dei pezzi di codice che vengono eseguiti dal Browser, sono un insieme di funzioni che ci permettono di eseguire operazioni come la gestione degli eventi, la manipolazione del DOM, la richiesta di dati da un server, l'esecuzione di funzioni dopo un certo intervallo di tempo, ecc.

Callback Hell - Il Purgatorio delle Callbacks 👿

Molto spesso ci possiamo trovare in situazioni dove abbiamo bisogno di risolvere il problema del Single Thread di JavaScript e di ritardare l'esecuzione di specifiche linee di codice o di eseguire una linea di codice, dire al computer "Eseguimi azione X quando questa linea di codice ha completato la sua esecuzione" e nel frattempo proseguire con il resto della logica che il nostro programma prevede. Come abbiamo visto questo può essere fatto con il concetto delle Callbacks ritardando l'esecuzione del codice all'interno della funzione setTimeout sulla base dei millisecondi dati nella Callback. Facciamo qualche esempio pratico per capire meglio.

Ti consiglio vivamente di provare questi esempi negli strumenti per sviluppatori del tuo browser.

setTimeout(() => {
  console.log('I am first');
}, 1000);


setTimeout(() => {
  console.log('I am second');
}, 2000);

setTimeout(() => {
  console.log('I am third');
}, 3000);

Nell'esempio stiamo chiamando tre volte la funzione setTimeout. Queste funzioni vengono eseguite una dopo l'altra, il codice all'interno invece viene eseguito rispettato l'intervallo di tempo di attesa che abbiamo passato come argomento alla funzione setTimeout.

Quindi tutto il codice viene eseguito velocemente quando la pagina nel browser viene caricata la prima volta e quello che il Browser comprenderà è questo:

  1. "Devo fare il console.log('I am first') tra 1 secondo"`
  2. "Devo fare il console.log('I am second') tra 2 secondi"`
  3. "Devo fare il console.log('I am third') tra 3 secondi"`

Questo perchè i millisecondi passati alle funzioni setTimeout sono relativi al momento in cui la pagina viene caricata per la prima volta. Quindi noi programmatori se vogliamo che questi console.log vengano eseguiti ad un secondo di distanza tra l'uno e l'altro dobbiamo tener conto di questo.

Come potremmo fare per dire semplicemente "Eseguimi ognuno di questi console log ad un secondo di distanza l'uno dall'altro"?

Possiamo fare così:

setTimeout(() => {
  console.log('I am first');
  setTimeout(() => {
    console.log('I am second');
    setTimeout(() => {
      console.log('I am third');
    }, 1000);
  }, 1000);
}, 1000);

Ora il Browser comprenderà che deve eseguire il primo console.log dopo 1 secondo, il secondo console.log dopo 2 secondi e il terzo console.log dopo 3 secondi e come vedi evitiamo la complicazione di dover tener traccia dei millisecondi passati alle funzioni setTimeout ma passiamo semplicemente 1000 millisecondi come argomento ad ognugno dei setTimeout il quale verrà eseguito dopo 1 secondo dall'esecuzione del setTimeout precedente.

Se da un lato abbiamo il problema del Single Thread che necessita di essere risolto e abbiamo le Callbacks come soluzione, dall'altro avremo il problema delle Callbacks che si annidano l'una dentro l'altra e che diventano sempre più complicate da leggere e da mantenere.

Infatti, questo che abbiamo appena fatto è un esempio di "Callback Hell", ovvero una situazione infernale in cui ci sono tante callbacks una all'interno dell'altra e dove inizia ad essere complessa da comprendere e prono ad errori. Questo è un problema molto comune quando si lavora con JavaScript e con le Callbacks.

JavaScript Promises

Le Promises sono un concetto introdotto in JavaScript con ES6 e sono un modo per gestire le Callbacks in modo più semplice e leggibile.

Possono essere un concetto confusionario quindi voglio prima di tutto introdurti bene al problema che cercano di risolvere e farti qualche esempio. In questo modo prepariamo un pò il terreno per poi esplorarle meglio.

Introduzione al problema

Che cos'è una JavaScript Promise? Una Promise rappresenta il concetto di una promessa. Una promise è un oggetto che rappresenta il completamento o il fallimento di un'operazione asincrona e può avere tre stati: pending, fulfilled o rejected.

Facciamo un esempio dove vogliamo eseguire un'azione solo se l'azione precedente è andata a buon fine. Prima che esistessero le Promises questo era un problema molto comune e veniva risolto con il concetto delle Callbacks e si finiva facilmente in un "Callback Hell" come abbiamo visto prima.

Per chiarire le idee vediamo un esempio pratico dove facciamo una serie di richieste HTTP ad un server e vogliamo eseguire un'azione solo se quella precedente è andata a buon fine.

Definiamo un metodo richiestaHttp che accetta come argomento tre argomenti: url, successCallback e errorCallback. Questo metodo simula una richiesta HTTP ad un server e ritorna un errore o i dati della richiesta HTTP.

function richiestaHttp(url, successCallback, errorCallback) {
  const ritardo = Math.floor(Math.random() * 4000) + 1000;
  setTimeout(() => {
    if (ritardo > 3000) {
      errorCallback('Errore nella richiesta HTTP');
    } else {
      successCallback('Richiesta HTTP eseguita con successo');
    }
  }, ritardo);
}

Che cosa sta succedendo in questo esempio? Stiamo definendo una funzione richiestaHttp. All'interno generiamo un numero randomico tra 1 e 3 secondi e lo assegnamo alla variabile ritardo. Dopodichè verifichiamo se ritardo è maggiore di 2 secondi. Se è maggiore di 2 secondi allora eseguiamo la callback errorCallback altrimenti eseguiamo la callback successCallback. Questo serve solo a farci simulare un errore o un successo della richiesta HTTP.

È una funzione di esempio per mostrare come una richiesta HTTP può andare a buon fine o meno.

Ora che abbiamo definito questa funzione la mettiamo in pratica. Chiameremo questa funzione richiestaHttp tre volte e gli assegniamo delle callbacks per ogni volta che la stiamo chiamando. Una callback viene eseguita in caso di successo mentre l'altra in caso di errore.

richiestaHttp('https://www.google.com', function(response) {
  console.log('Richiesta 1 eseguita con successo');
  console.log(response);
  richiestaHttp('https://www.google.com', function(response) {
    console.log('Richiesta 2 eseguita con successo');
    console.log(response);
    richiestaHttp('https://www.google.com', function(response) {
      console.log('Richiesta 3 eseguita con successo');
      console.log(response);
    }, function(error) {
      console.log('Errore nella richiesta 3');
      console.log(error);
    });
  }, function(error) {
    console.log('Errore nella richiesta 2');
    console.log(error);
  });
}, function(error) {
  console.log('Errore nella richiesta 1');
  console.log(error);
});

Questo è ovviamente soltanto un esempio ma nella realtà potremmo aver bisogno di fare una serie di richieste HTTP ed eseguire un'azione soltanto se quella precedente è andata a buon fine, o altrimenti eseguirne un'altra differente.

Nell'esempio possiamo vedere come la Callback Hell crea un codice molto difficile da leggere, specialmente se non siamo stati noi a scriverla e quindi non sappiamo effettivamente qual è lo scopo di quella logica e che cosa sta succedendo al suo interno.

Le Promises quindi sono arrivate e ci hanno "promesso" di salvarci dalle Callbacks Hell 😁

Soluzione al problema - Le Promises

Prima abbiamo definito una funzione richiestaHttp che accettava tre argomenti: url, successCallback e errorCallback. Questo perchè dovevamo fornire delle callbacks per gestire il successo e l'errore della richiesta HTTP.

Adesso vediamo come definire lo stesso tipo di logica che richiestaHttp provava di fare ma questa volta utilizzando il concetto delle Promises.

function richiestaHttp(url) {
  return new Promise((resolve, reject) => {
    const ritardo = Math.floor(Math.random() * 4000) + 1000;
    setTimeout(() => {
      if (ritardo > 3000) {
        reject('Errore nella richiesta HTTP');
      } else {
        resolve('Richiesta HTTP eseguita con successo');
      }
    }, ritardo);
  });
}

All'interno della Promise facciamo la stessa logica con la variabile ritardo generando un numero randomico per decidere se risolvere o rigettare la Promise. Questo è per simulare una richiesta HTTP che può andare o meno a buon fine. Noi lo facciamo in modo randomico generando un numero randomico all'interno della funzione e controllando se questo numero è maggiore di 5000.

Ora che abbiamo definito la nostra Promise proviamo a chiamarla e vedere che cosa ci ritorna.

const risultato = richiestaHttp('https://www.google.com');
console.log(risultato); // Promise { <pending> }

Noteremo che il risultato non è quello che ci aspettavamo. Il risultato è un oggetto di tipo Promise con uno stato pending se saremo veloci a fare il log nella console prima che la Promise venga risolta. Dopo quanto tempo la Promise viene risolta? In questo caso dipende dal numero randomico che abbiamo generato all'interno della Promise che stiamo dando come tempo di attesa alla funzione setTimeout.

Se provassimo a ripetere il console.log del risultato dopo qualche secondo o giusto il tempo che la Promise venga risolta, noteremmo che il risultato adesso sarà quello che ci aspettavamo e avrà un risultato di resolved oppure rejected in base al numero randomico generato.

console.log(risultato); // Promise { <resolved> }

In questo caso il mio esempio ha generato un numero randomico inferiore ai 3 secondi che stiamo controllando e quindi la Promise è stata risolta con successo.

Prova anche tu nel tuo Browser a fare questo esempio.

Se il numero randomico generato è maggiore di 3 secondi allora la Promise viene rigettata.

Then e Reject

Ora che abbiamo visto come una funzione viene risolta o rigettata, vediamo come possiamo utilizzare delle callbacks che vengono eseguite a seconda del risultato della Promise.

richiestaHttp('https://www.google.com')
  .then((risultato) => {
    console.log('Richiesta HTTP eseguita con successo');
    console.log(risultato);
  })
  .catch((errore) => {
    console.log('Errore nella richiesta HTTP');
    console.log(errore);
  });

Che cosa sta succedendo qui?

Abbiamo collegato la callback then e la callback catch alla Promise e gli stiamo passando una logica all'interno. La callback then viene eseguita in caso di successo (resolved) mentre la callback catch viene eseguita in caso di errore (rejected).

Quindi abbiamo esattamente la stessa logica di quando avevamo usato successCallback e errorCallback ma questa volta abbiamo usato le Promise ed abbiamo un codice molto più pulito e più semplice da comprendere.

Prova ad eseguire questo esempio nel tuo Browser. Puoi copiare ed incollare la logica nella console ma assicurati di aver definito la funzione richiestaHttp prima di eseguire il codice altrimenti otterrai solo un errore.

Quando eseguirai il codice nella console e aspetterai qualche secondo noterai che il console.log verrà eseguito in modo autonomo dopo che la Promise sarà stata risolta o rigettata.

Il bello delle Promises

Durante l'introduzione al problema che le Promise risolvono abbiamo visto come il codice diventa molto difficile da leggere quando abbiamo bisogno di eseguire una serie di richieste HTTP e gestire il successo o l'errore di ogni singola richiesta.

Vediamo come possiamo risolvere questo stesso problema utilizzando le magia del then che le Promise ci offrono.

richiestaHttp('https://www.google.com')
  .then((risultato) => {
    return richiestaHttp('https://www.google.com');
  })
  .then(() => {
    return richiestaHttp('https://www.google.com');
  })
  .then(() => {
    return richiestaHttp('https://www.google.com');
  })
  .then(() => {
    console.log('Tutte le richieste HTTP sono state eseguite con successo');
  })
  .catch((errore) => {
    console.log('Errore nella richiesta HTTP');
    console.log(errore);
  });

L'esempio ci mostra come possiamo facilmente eseguire una richiesta dopo l'altra con l'utilizzo di then che dirà: "Quando la Promise viene risolta esegui la callback e poi esegui la prossima richiesta HTTP". Nel caso in cui una delle richieste HTTP venga rigettata, la callback catch verrà eseguita e non verranno eseguite le altre richieste HTTP.

Molto più semplice e pulito rispetto all'esempio precedente nell'introduzione al problema, vero? Questo è il vero potere delle Promise. 💪

Async Keyword

Stiamo piano piano studiando tutto ciò che gira intorno all'argomento principale della sezione Async di questo articolo. Abbiamo appena visto che cosa sono le Callbacks e come possiamo utilizzare le Promise per avere una sintassi ordinata e pulita.

Questo che abbiamo visto finora è importante capirlo bene perchè prepara il terreno per capire come funziona il concetto di Async/Await.

Le Promise sono infatti importanti da capire perchè sono il motore che ci permette di utilizzare le funzionalità di Async/Await.

Vediamo quindi che cosa s'intende per Async e la sintassi che ci permette di utilizzare questo concetto.

Iniziamo con delle regole di base di async:

  • le funzioni che utilizzanoo async ritornano sempre una Promise
  • se la funzione ritorna un valore, la Promise sarà risolta con quel valore
  • se la funzione genera un errore, la Promise sarà rigettata con quel valore
async function esempioAsyncResolved() {
  return 'ciao';
}
esempioAsyncResolved();
// Promise { <resolved>: 'ciao' }

async function esempioAsyncRejected() {
  throw new Error('Errore');
}
esempioAsyncRejected();
// Promise { <rejected>: Error: Errore }

Quindi nel momento in cui utilizziamo la keyword async davanti alla definizione di una funzione, questa ritornerà sempre una Promise come output. Anzichè ritornarci la stringa ciao per esempio, vedi che ci ritorna un oggetto Promise che come valore resolved ha la stringa ciao.

Await Keyword

Introduciamo ora la keyword await che ci permette di aspettare che una Promise venga risolta prima di eseguire il resto del codice.

Quindi utilizzando await possiamo simulare una funzione asincrona come se fosse sincrona.

async function esempioAsyncAwait() {
  const risultato = await richiestaHttp('https://www.google.com');
  console.log(risultato);
}

esempioAsyncAwait();

Quando esegui questo codice noterai che il console.log all'interno di esempioAsyncAwait verrà eseguito solo quando la Promise di richiestaHttp sarà stata risolta. questo perchè stiamo utilizzando await davanti a richiestaHttp perchè in questo caso vogliamo aspettare che la Promise venga risolta prima di eseguire il resto del codice.

Con l'utilizzo di async ed await possiamo scrivere codice asincrono in modo molto più pulito e comprensibile specialmente quando stiamo lavorando con della logica complessa.

APIs - Application Programming Interface

Che cosa significa fare una richiesta HTTP ?

Prova ad aprire il Browser e scrivere nella barra degli indirizzi https://www.google.com e premi invio. Il Browser ti mostrerà la pagina di Google. Come? Questo perchè il Browser ha fatto una richiesta HTTP a Google per ottenere la pagina di Google, ha ricevuto la risposta e ha mostrato la pagina di Google.

Questa sorta di richiesta HTTP è una richiesta HTTP fatta da un Browser. Ma che succede se vogliamo fare una richiesta HTTP dal nostro codice che scriviamo in VS Code e non direttamente da un Browser?

In questa sezione impararemo come fare richieste APIs da codice JavaScript che appunto ci permettono di fare esattamente questo che abbiamo appena descritto, ovvero fare richieste HTTP e ottenere risposte contenenti dati.

Fai attenzione dove ho scritto "ottenere risposte contenenti dati". Quando apriamo un sito web, il Browser fa una richiesta HTTP e ottiene una risposta che contiene HTML, CSS e JavaScript e questo serve a creare la pagina in modo corretto che vediamo nel Browser.

Proviamo ad immaginare che siamo noi a costruire un sito web, per esempio un sito di notizie della nostra città e vogliamo ottenere i dati delle notizie da varie risorse esterne. Non ci servono avere immagini, CSS o JavaScript ma solo i dati delle notize. Come possiamo fare?

In questo caso possiamo far utilizzo delle API. Dovremo cercare delle API disponibili nel web che offrono i dati delle notizie della nostra città e poi fare una richiesta HTTP a questa/e API per ottenere i dati delle notizie come risposta.

Andiamo a vedere meglio che cosa sono le API.

Approfondimento

Le API sono dei servizi che offrono dei dati, molto spesso in formato JSON, che possono essere utilizzati da altri programmi per creare applicazioni.

Perchè l'acronimo "Application Programming Interface" ?

Un API può essere vista come una forma di comunicazione tra due parti. Una parte è il nostro codice che scriviamo in VS Code e l'altra parte è il servizio che offre i dati.

Quando parlo di servizio che offre dati in realtà questo non dev'essere necessariamente un servizio esterno ma potrebbe semplicemente essere un altro programma che stiamo scrivendo noi stessi e che come compito ha quello di ricevere una richiesta e di ritornare la corretta risposta che appunto conterrà i dati richiesti.

Detto ciò, quando parliamo di APIs solitamente ci riferiamo alle Web APIs, ovvero delle APIs che sono disponibili sul web e che espongono i cosiddetti endpoints o punti d'ingresso che sono degli URL che possono essere chiamati per ottenere i dati in risposta.

Solitamente questi dati tornano nella risposta in formato JSON. Questo formato ci permette di lavorare con i dati in modo molto semplice e veloce.

Vediamo un esempio.

Proviamo a fare una richiesta HTTP a una API e vediamo cosa otteniamo come risposta. Per fare questo utilizzeremo un'API gratuita che offre come dati degli scherzi su Chuck Norris (per chi non se lo ricordasse il buon vecchio Walker Texas Ranger).

Ti starai chiedendo

Ma perchè tra tante cose, proprio gli scherzi su Chuck Norris?

Perchè Chuck Norris è il migliore.

Scherzi a parte, ci sono tante API disponibili sul web, alcune sono gratuite e altre a pagamento. Questa era una di quelle disponibili gratuitamente.

Proseguiamo con la richiesta API e vediamo cosa otteniamo come risposta. Scrivi il seguente URL nella barra degli indirizzi del Browser e premi invio.

https://api.chucknorris.io/jokes/random

Risultato

{"categories":[],"created_at":"2020-01-05 13:42:19.897976","icon_url":"https://assets.chucknorris.host/img/avatar/chuck-norris.png","id":"2QMqYUENTYq6TkVmgxcKjA","updated_at":"2020-01-05 13:42:19.897976","url":"https://api.chucknorris.io/jokes/2QMqYUENTYq6TkVmgxcKjA","value":"Chuck Norris has a beard made of Brillo Pads"}

Come vedi non otteniamo immagini, CSS o JavaScript ma otteniamo un oggetto JSON che contiene dati.

Nel nostro programma eseguiamo questa richiesta HTTP, otteniamo la risposta e poi possiamo lavorare con i dati e usare solo quello che ci serve.

Questo è solo un esempio di come possiamo utilizzare delle APIs esterne per ottenere dei dati che sono esterni al nostro programma e che possiamo utilizzare per creare qualsiasi tipo di applicazione.

Vogliamo creare un sito web che mostra gli scherzi su Chuck Norris? Possiamo farlo.

Vogliamo creare un sito web che mostra i dati meteo della nostra città? Possiamo farlo. Basta usare le API che offrono i dati meteo.

Vogliamo creare un sito web che mostra i dati delle notizie della nostra città? Possiamo farlo. Basta usare le API che offrono i dati delle notizie.

Vogliamo creare un sito web che mostra i dati delle partite di calcio della nostra squadra preferita? Possiamo farlo. Basta usare le API che offrono i dati delle partite di calcio.

Questi sono solo alcuni esempi di come possiamo utilizzare le APIs per creare le nostre applicazioni.

Questo è un sito che offre un elenco di alcune delle APIs disponibili sul web: https://rapidapi.com/collection/best-free-apis

JSON

JSON è un formato di dati che viene utilizzato per scambiare dati tra applicazioni. È il formato di dati che oggi come oggi vedrai più spesso quando lavorerai con le APIs. Ci sono altri formati come per esempio XML. Quest'ultimo veniva utilizzato particolarmente in passato ma ad oggi è utilizzato molto molto meno e per questo ti consiglio di focalizzarti unicamente su JSON.

Iniziamo con il chiarire l'acronimo di JSON: JavaScript Object Notion.

{
  "name": "Gianluca",
  "age": 30,
  "hobbies": ["coding", "reading", "gaming"]
}

Come vedi JSON è un formato di dati che è molto simile ad un oggetto JavaScript.

In realtà quando effettuiamo una richiesta HTTP ad un API otterremo si un oggetto JSON ma questo sarà contenuto all'interno di una stringa.

'{"name":"Gianluca","age":30,"hobbies":["coding","reading","gaming"]}'

Quindi se volessimo ottenere il valore della proprietà name non possiamo chiamare il valore name direttamente ma dobbiamo prima convertire la stringa in un oggetto JavaScript e poi ottenere il valore della proprietà name. Questo dimostra che JSON è un formato dati molto simile a JavaScript ma non è un oggetto JavaScript.

Vediamo un esempio nel browser. Apriamo la console del browser ed incolliamo il seguente codice:

const persona = '{"name":"Gianluca","age":30,"hobbies":["coding","reading","gaming"]}'
persona.name // undefined

Come vedi otteniamo undefined perchè non possiamo accedere direttamente alla proprietà name perchè non è un oggetto JavaScript ma una stringa.

Ora convertiamo la stringa in un oggetto JavaScript utilizzando il metodo JSON.parse().

const persona = '{"name":"Gianluca","age":30,"hobbies":["coding","reading","gaming"]}'
const personaObj = JSON.parse(persona)
personaObj.name // Gianluca

Come noterai, dopo aver convertito la stringa in un oggetto JavaScript con il metodo JSON.parse possiamo accedere direttamente alla proprietà name e ottenere il valore Gianluca. Bello, no? :)

Quindi con il metodo JSON.parse() possiamo convertire una stringa JSON in un oggetto JavaScript.

Come facciamo se volessimo convertire un oggetto JavaScript in una stringa JSON? Possiamo farlo utilizzando il metodo JSON.stringify().

const persona = {
  name: 'Gianluca',
  age: 30,
  hobbies: ['coding', 'reading', 'gaming']
}

const personaString = JSON.stringify(persona)
personaString // '{"name":"Gianluca","age":30,"hobbies":["coding","reading","gaming"]}'

Ora che abbiamo un'idea di cosa sia un API e che tipo di risposta HTTP possiamo ricevere da un API, vediamo come possiamo effettuare una richiesta HTTP ad un API.

Strumenti di lavoro - Postman

Abbiamo visto come possiamo effettuare una richiesta HTTP ad un API dal Browser. Questo funziona benissimo a parte che non è molto chiaro e può diventare molto scomodo se dobbiamo fare molte richieste HTTP ad un API.

Per questo motivo ti consiglio di utilizzare un programma gratuito che ti permette di effettuare richieste HTTP ad un API in modo molto più chiaro e semplice.

Questo programma si chiama Postman, basta crearti un account e sei pronto a partire.

Puoi utilizzare la versione web nel tuo browser o puoi scaricare il programma dal seguente link: https://www.postman.com/downloads/

Se utilizzi Windows a te comparirà la versione per Windows. Come noti dall'immagine puoi registrarti e poi decidi se scaricarti il software o utilizzare la versione web.

Questo semplice programma ci facilita la vita quando dobbiamo provare delle APIs. Vediamo come possiamo utilizzarlo.

Per prima cosa dobbiamo entrare nella nostra Workspace e creare una nuova collezione. Una collezione è un insieme di richieste HTTP che possiamo raggruppare per argomento.

Segui le immagini per creare una nuova collezione:

Adesso dovresti ritrovarti con una pagina simile a questa:

Qui possiamo aggiungere l'URL dell'API che vogliamo provare. Per esempio proviamo un'altra API gratuita che ci permette di ottenere idee randomiche su attività che possiamo fare.

L'URL dell'API è il seguente: https://www.boredapi.com/api/activity

Ci sono una serie d'informazioni utili che otteniamo. Al momento non c'importa andare a capire che cosa significhino perchè stiamo per andarlo a scoprire nelle prossime lezioni. Quello che importa è che abbiamo Postman pronto all'uso! :)

I metodi HTTP

Quando facciamo una richiesta HTTP ad un API possiamo utilizzare diversi metodi HTTP. Ogni metodo ha un suo socopo ed utilità.

Diamo un'occhiata a questi metodi su Postman:

Questa è la lista dei metodi HTTP che possiamo notare su Postman quando clicchiamo su GET di fianco all'URL.

A che cosa servono tutti questi metodi? Vediamolo insieme.

Ad esempio, il metodo GET che in inglese significa ottenere è utilizzato per ottenere delle risorse da un server. Il metodo POST invece è utilizzato per inviare dati ad un server per creare una nuova risorsa.

Ad esempio, finora abbiamo visto come ottenere uno scherzo su Chuck Norris o come ottenere delle attività randomiche. Se stessi utilizzando per esempio l'API di Facebook potremmo usare il metodo GET per ottenere la lista dei nostri amici.

Se invece volessimo aggiungere un nuovo amico e quindi creare una nuova risorsa sui server di Facebook potremmo utilizzare il metodo POST.

Ti ho fatto vedere l'intera lista ma in realtà non tutti i metodi sono utilizzati con frequenza. I metodi più utilizzati sono GET, POST, PUT e DELETE.

Le APIs che offrono questi quattro metodi vengono chiamate REST APIs. Questo perchè questi metodi sono basati su un modello di architettura chiamato REST.

In ogni caso, non complichiamo le cose più di quanto debbano esserlo. Per ora ti basta sapere che esistono diversi metodi HTTP e che ogni metodo ha un suo scopo. Noi ci focalizzeremo principalmente sul metodo GET.

HTTP Status codes

Quando effettuiamo una richiesta HTTP ad un API ci sono vari modi per capire se la richiesta è andata a buon fine. Ovviamente, se stessimo facendo una richiesta GET, uno di questi potrebbe semplicemente vedere se la risposta contiene i dati che ci aspettiamo.

Ma non sempre è così semplice. Ad esempio, se stessimo facendo una richiesta POST per aggiungere un nuovo amico su Facebook, non possiamo semplicemente vedere se la risposta contiene i dati che ci aspettiamo o meno. Il modo corretto per controllare se una richiesta HTTP sia andata a buon fine o meno è quello di controllare l' HTTP Status code della risposta.

Quando abbiamo fatto la richiesta HTTP alla "boredapi" abbiamo in realtà ricevuto una risposta HTTP con un HTTP Status code di 200. Questo significa che la richiesta è andata a buon fine.

Come puoi immaginare esistono diversi HTTP Status codes. Ogni HTTP Status code ha un suo significato. Ad esempio, il HTTP Status code 200 significa che la richiesta è andata a buon fine. Il HTTP Status code 404 significa che la risorsa richiesta non è stata trovata.

Vediamo insieme alcuni dei HTTP Status codes più comuni:

  • 200 - OK
  • 201 - Created
  • 204 - No Content
  • 400 - Bad Request
  • 401 - Unauthorized
  • 403 - Forbidden
  • 404 - Not Found
  • 500 - Internal Server Error

Questi sono solo alcuni dei HTTP Status codes più comuni. Puoi trovare una lista completa di tutti i HTTP Status codes su questo sito.

Query Parameters

Quando facciamo una richiesta HTTP vorremmo poter passare delle informazioni alla nostra richiesta per poter ottenere informazioni più specifiche.

In base al tipo di API che stiamo utilizzando potremmo dover passare delle informazioni in diversi modi. Ad esempio, potremmo dover passare delle informazioni come parte dell'URL o come parte del corpo della richiesta HTTP.

Uno dei modi più comuni per passare delle informazioni ad una richiesta HTTP è quello di utilizzare i Query Parameters.

Vediamo come possiamo passare dei Query Parameters alla Bored API che abbiamo utilizzando prima per ottenere delle attività randomiche.

Se noti la risposta JSON che abbiamo ottenuto dalla richiesta GET che abbiamo fatto prima, noterai che ci sono diversi campi. Uno di questi campi è key. Questo campo ci dice che l'attività che ci è stata restituita ha una chiave assegnata. Questa chiave è univoca per ogni attività.

Ora anzichè fare un'altra richiesta HTTP ed ottenere un'attività diversa, come facciamo se volessimo fare una nuova richiesta HTTP ed assicurarci di ottenere la stessa attività?

Anzitutto dobbiamo consultare la documentazione dell'API che stiamo utilizzando. Infatti devi sapere che ogni API è diversa e quindi ogni API che si rispetti ha una documentazione che ti spiega come utilizzarla.

Diamo quindi un'occhiata a questa documentazione: https://www.boredapi.com/documentation

Dalla documentazione possiamo notare che esiste un Query Parameter chiamato key.

Proviamo quindi a fare una nuova richiesta HTTP utilizzando key come Query Parameter.

Un'ultima cosa che devi sapere prima di poter procedere. I Query Parameters seguono il cosiddetto formato di key=value pair. Ovvero, una coppia di chiave e corrispondente valore. In questo caso la nostra chiave si chiama appunto key ed il valore sarà la chiave che abbiamo ottenuto dalla nostra prima richiesta HTTP.

https://www.boredapi.com/api/activity?key=7091374

Nota come ho passato l'URL https://www.boredapi.com/api/activity, dopodichè ho aggiunto un punto interrogativo e poi finalmente ho aggiunto la key-value pair key=7091374 ed ho ottenuto come risposta la stessa attività che abbiamo ottenuto prima: "Make a simple musical instrument" ("Crea un semplice strumento musicale").

Fino a prima del punto interrogativo abbiamo l'URL della nostra richiesta HTTP. Dopo il punto interrogativo abbiamo i nostri Query Parameters. A volte vorremmo poter passare più di un Query Parameter alla nostra richiesta HTTP. In questo caso dovremmo separare i nostri Query Parameters con un &.

Facciamo un esempio pratico. Questa volta utilizzeremo due Query Parameters. Il primo sarà minprice ed il secondo sarà maxprice. Questo per dare un range di prezzo per le attività che vogliamo ottenere.

Come faccio a sapere che con questa Bored API posso passare questi due Query Parameters? Semplice, consulto la documentazione.

Quando si lavora con le API è fondamentale fare sempre riferimento alla sua documentazione ufficiale.

Se un'API non dispone di una documentazione ufficiale, allora questo è gia un forte segnale che non sarà un'API affidabile.

Proseguiamo quindi con la richiesta:

https://www.boredapi.com/api/activity?minprice=0.1&maxprice=0.2

Come puoi notare, questa volta abbiamo ottenuto un'attività che ha un prezzo compreso tra 0.1 e 0.2.

Fetch API

Okay, abbiamo visto come possiamo fare delle richieste HTTP utilizzando il browser, abbiamo visto come farle utilizzando un tool come Postman che ci semplifica la vita quando vogliamo provare un API e abbiamo visto come possiamo passare dei Query Parameters alla nostra richiesta HTTP.

Ora è arrivato il momento di vedere come possiamo fare delle richieste HTTP utilizzando JavaScript!

Voglio anticiparti che nella prossima sezione parleremo di Axios che è una libreria esterna di JavaScript che ci permette di fare delle richieste HTTP in modo ancora più semplice. Axios si basa su Fetch di JavaScript quindi è giusto che prima di parlare di Axios parliamo di Fetch e di come funziona.

Testiamo il seguente codice. Un modo sarebbe quello di creare una file index.html e un file script.js, poi fare riferimento a script.js nel file index.html ed infine aprire il file index.html nel browser cosí che possiamo vedere il risultato del codice nel file script.js nella console del browser.

Il modo più veloce invece è quello di aprire direttamente il tuo browser (anche dalla pagina in cui stai leggendo questo articolo) e aprire la console. Poi copia e incolla il codice nella console.

fetch('https://www.boredapi.com/api/activity')
  .then((response) => {
    console.log('RESOLVED', response);
    return response.json();
  })
  .catch((error) => {
    console.log('ERROR', error);
  });

Nota come facciamo la richiesta HTTP e riceviamo una risposta in formato JSON, motivo per cui utilizziamo response.json() per convertire la risposta in un oggetto JavaScript.

Sarebbe bello se potessimo fare una richiesta HTTP e non doverci ricordare di fare response.json() per convertire la risposta in un oggetto JavaScript. Per questo e per altri motivi che scopriremo man mano, voglio introdurti alla libreria Axios.

Axios

Axios è una libreria esterna di JavaScript che ci permette di fare delle richieste HTTP in modo molto semplice.

Se è la prima volta che incontri il concetto di libreria esterna, allora ti spiego che cos'è e perchè esistono.

Immagina di dover costruire una casa da zero. Necessiti di tutti i materiali e gli attrezzi necessari come mattoni, cemento, travi, ecc. Cosa succede se qualcuno ti regalasse una scatola piena di attrezzi e materiali già pronti all'uso? Risparmieresti molto tempo e fatica.

Le librerie esterne nella programmazione sono come quella scatola di attrezzi. Sono collezioni di codici e funzioni già pronte all'uso che gli sviluppatori possono utilizzare nei propri programmi.

Per esempio se stessimo costruendo un programma che permette all'utente di selezionare una data da un calendario avremmo due opzioni:

  1. costruire tutto il codice per il calendario da zero
  2. utilizzare una libreria esterna che ci permette di utilizzare un calendario già pronto all'uso e quindi integrare questa libreria esterna nel nostro sito ed avere la funzione del calendario pronta all'uso

In pratica le librerie esterne ci permettono di utilizzare codice testato e pronto all'uso e quindi di farci focalizzare sulla logica del programma che stiamo cercando di costruire e non sulla logica del singolo pezzo.

Axios è una libreria esterna che ci facilita le richieste HTTP. Solitamente questa libreria esterna viene usata nel Backend quindi è stata disegnata per essere facilmente incorporata in un progetto Backend. Noi la utilizzeremo per il Frontend.

Questa è la repository di Axios su GitHub: https://github.com/axios/axios

Se hai un file index.html e un file script.js già pronti all'uso allora aggiungi questa parte di codice nel tuo file index.html sopra alla riga dove stai linkando il tuo file script.js:

<script src="https://cdn.jsdelivr.net/npm/axios@1.1.2/dist/axios.min.js"></script>

Se non hai ancora un file index.html e un file script.js allora iniziamo con il creare un file index.html ed aggiungere questo codice:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>JavaScript Advanced</title>
</head>
<body>
  <h1>JavaScript Advanced</h1>
  <script src="https://cdn.jsdelivr.net/npm/axios@1.1.2/dist/axios.min.js"></script>
  <script src="script.js"></script>
</body>
</html>

E poi creiamo un file script.js e aggiungiamo questo codice:

axios
  .get('https://www.boredapi.com/api/activity')
  .then((response) => {
    console.log('RESOLVED', response);
  })
  .catch((error) => {
    console.log('ERROR', error);
  });

Che cosa sta succedendo qui? Abbiamo sostituito fetch con axios e abbiamo aggiunto .get prima dell'URL. Cosí è come facciamo una GET request con Axios.

La differenza principale è come gestiamo la risposta. Con fetch abbiamo dovuto fare response.json() per convertire la risposta in un oggetto JavaScript. Con Axios invece non abbiamo bisogno di fare response.json() perché Axios lo fa automaticamente per noi.

JavaScript Error Handling - Gestione degli errori

Immagina che stessi cucinando una ricetta. Inizi a preparare la ricettà e a metà strada ti accorgi che hai dimenticato un ingrediente principale o che qualcosa è andato storto, per esempio hai messo troppo sale. Che cosa faresti in questo caso?

Anzichè dover buttare tutto quello che abbiamo preparato, proviamo a rimediare al problema.

Il concetto di try e catch è molto simile a questo. Prova a immaginare che stai facendo una richiesta HTTP e che qualcosa va storto. In questo caso invece di dover interrompere il programma e non fare nulla, possiamo provare a rimediare al problema in modo che il programma possa continuare a funzionare.

try {
  // qui prepari la ricetta (fai la richiesta HTTP)
} catch (e) {
  // qui rimedi al problema (gestisci l'errore)
}

Nella parte del try mettiamo il codice che potrebbe generare un errore. Se l'errore non si verifica, allora il codice viene eseguito senza problemi.

Nella parte del catch mettiamo il codice che gestisce l'errore. Se l'errore si verifica, allora il codice viene eseguito.

Se non si dovesse verificare nessun errore, allora il codice nella parte del catch non verrà mai eseguita.

Per esempio proviamo a fare un'addizione di y + 5 dove y non è mai stata creata e quindi creiamo volontariamente un errore. Aggiungiamo il seguente codice nel nostro file script.js e poi guardiamo nella console del browser:

try {
  let x = y + 5;
  console.log(x);
} catch (errore) {
  console.error("C'è stato un errore: " + errore.message);
}

Il risultato nel browser:

Molto spesso la logica di try e catch viene usata quando si fanno richieste HTTP. Nel progetto che andremo a costruire infatti utilizzeremo try e catch proprio per questo! Vediamo come.

Progetto - Bored Activity Generator

In questo progetto andremo a mettere in pratica varie conoscenze che abbiamo acquisito in quest'articolo. La pratica è la migliore forma di apprendimento!

Vogliamo creare un generatore di attività casuali. Questo generatore ci permetterà di generare attività casuali quando non sappiamo cosa fare.

Creiamo un file index.html e aggiungiamo questo codice:

<!DOCTYPE html>
<html>
  <head>
    <title>Generatore di attività casuali</title>
  </head>
  <body>
    <h1>Generatore di attività casuali</h1>
    <table>
      <thead>
        <tr>
          <th>Attività</th>
          <th>Tipo</th>
          <th>Partecipanti</th>
        </tr>
      </thead>
      <tbody id="activity-table"></tbody>
    </table>
    
    <button id="new-activity-button">Nuova attività</button>

    <div id="message-display"></div>

    <script src="https://unpkg.com/axios/dist/axios.min.js"></script>
    <script src="script.js"></script>
  </body>
</html>

Qui stiamo creando un semplice file HTML con un titolo, una tabella e un bottone. Abbiamo una div message-display dove aggiungeremo un messaggio di successo o di errore ogni volta che generiamo una nuova attività.

Adesso andiamo a creare il nostro file script.js e aggiungiamo questo codice:

const activityTable = document.getElementById('activity-table');
const newActivityButton = document.getElementById('new-activity-button');
const messageDisplay = document.getElementById('message-display');
const apiUrl = 'https://www.boredapi.com/api/activity/';

const getActivity = async () => {
  try {
    const response = await axios.get(apiUrl);
    console.log(response.data);
    const activity = response.data;
    const row = document.createElement('tr');
    row.innerHTML = `
      <td>${activity.activity}</td>
      <td>${activity.type}</td>
      <td>${activity.participants}</td>
    `;
    activityTable.appendChild(row);
    messageDisplayCustomiser('Nuova attività aggiunta', 'green');
  } catch (error) {
    console.error(error);
    messageDisplayCustomiser('Si è verificato un errore', 'red');
  }
};

const messageDisplayCustomiser = (message, color) => {
  messageDisplay.textContent = message;
  messageDisplay.style.backgroundColor = color;

  setTimeout(() => {
    messageDisplay.textContent = '';
    messageDisplay.style.backgroundColor = '';
  }, 2000);
}

newActivityButton.addEventListener('click', () => {
  getActivity();
});

Che cos'è tutta questa roba?! 🤦‍♂️ Non preoccuparti, vediamo cosa fa tutto questo codice.

Innanzitutto utilizziamo document.getElementById per ottenere tutti gli elementi presenti sul DOM che ci servono per manipolarli nel nostro codice.

Dopodichè assegniamo l'URL della Bored API a una costante apiUrl. Cosí che possiamo utilizzarla facilmente nel nostro codice.

Poi passiamo alla definizione della funzione getActivity. Questa funzione è async perché utilizzeremo await all'interno di essa. Questo ci permette di aspettare che la richiesta HTTP sia completata prima di continuare con il codice.

Dentro la funzione getActivity abbiamo un blocco try e catch. Il blocco try contiene il codice che potrebbe generare un errore, in questo caso la richiesta HTTP. Se la richiesta HTTP non genera alcun errore, allora il codice viene eseguito senza problemi.

Quando otteniamo la risposta HTTP, la salviamo in una costante response.

Poi facciamo un console log della risposta per analizzare i dati nella console del browser. Questo non è necessario e ci serve solamente per vedere la risposta nella console e capire come possiamo estrapolare la parte che ci occore. Una volta che sappiamo come possiamo estrapolare l'attività, prendiamo questi dati e li salviamo in una costante activity.

A questo punto creiamo una nuova riga row che aggiungeremo alla nostra tabella.

Una volta che abbiamo creato la row possiamo aggiungere i dati provenienti dalla risposta HTTP e questo lo facciamo con innerHTML. Aggiungiamo l'attività, il tipo ed il numero di partecipanti alla riga.

Infine aggiungiamo la riga alla tabella e chiamiamo la funzione messageDisplayCustomiser per mostrare un messaggio di successo.

Dopo la funzione getActivity abbiamo una funzione messageDisplayCustomiser. In questa funzione stiamo assegnando il messaggio ed il colore alla div con ID message-display e poi stiamo impostando un timer di 2 secondi per resettare il tutto.

Perchè abbiamo creato una funzione? Perchè la stiamo utilizzando due volte, nel try e nel catch e quando lo stesso tipo di logica viene utilizzato più volte, è una buona pratica metterla in una funzione.

Infine aggiungiamo un event listener al bottone new-activity-button che chiama la funzione getActivity ogni volta che viene cliccato.

JavaScript Prototypes, Classi e OOP

Con questa sezione stiamo entrando nel vivo della programmazione e di JavaScript. La programmazione ad oggetti e come funziona in JavaScript.

Prima di procedere voglio darti una breve introduzione ai Prototypes. Ti anticipo che sono un concetto leggermente "del passato" ma è importante avere giusto una base di comprensione perchè nonostante siano "del passato" li vedremo ancora utilizzare in JavaScript in varie occasioni.

Prototypes

A volte le persone fanno confusione quando si parla di JavaScript prototypes ed è un concetto che può fare sempre un pò di "paura" quando si cerca di impararlo ma in realtà voglio darti una buona notizia: non è così difficile come potrebbe sembrare. Ti farò un esempio pratico per farti capire come funzionano i Prototypes o Prototipi.

In OOP, Object Oriented Programming o Programmazione ad Oggetti, praticamente tutto è un oggetto. Che cosa s'intende per oggetto? Un oggetto è una collezione di proprietà e metodi.

Quando creiamo una nuova stringa ad esempio, questa stringa è un oggetto. Questa stringa ha delle proprietà e dei metodi che possiamo utilizzare e che noi non dobbiamo preoccuparci di dover creare.

Questi metodi che possiamo utilizzare chiamati anche built-in methods (ovvero metodi pre-definiti) sono definiti all'interno di una sorta di modello. Ogni oggetto ha un suo modello con delle proprietà e metodi pre-definiti. Questi modelli si chiamano appunto Prototypes.

Quindi, quando creiamo una stringa ad esempio, questa stringa ha un suo prototype dal quale eredita proprietà e metodi built-in.

Ad esempio, se volessimo sapere quanti elementi sono presenti in un array potremmo usare il metodo length e questo metodo è definito nel prototype dell'oggetto Array.

Puoi testare questo codice nella console del tuo browser.

['a', 'b', 'c'].length // 3

Come facciamo ad avere questo metodo length a nostra disposizione? Perchè è stato creato quando è stato sviluppato il linguaggio di programmazione JavaScript ed è stato messo a disposizione nel Prototype dell'oggetto Array.

Se volessimo vedere quali sono i metodi presenti nel prototype dell'array possiamo per esempio scrivere un'array vuota e poi cliccare con il mouse sulla freccia verso destra che si trova accanto all'array.

Puoi vedere che l'Array ha tanti metodi pre-definiti nel suo prototype. Questi sono tutti metodi che noi ereditiamo.

E se volessimo aggiungere un metodo custom al prototype dell'array? Possiamo farlo con la keyword prototype.

Array.prototype.myCustomFeature = 'cool!'

Adesso possiamo utilizzare questo metodo custom nel nostro codice.

['a', 'b', 'c'].myCustomFeature // 'cool!'

E infatti il metodo myCustomFeature adesso sarà disponibile per qualsiasi nuova array che creeremo.

In generale aggiungere oggetti e metodi al prototype di un oggetto è una pratica non consigliata perchè potrebbe creare problemi di conflitto con altri metodi che potrebbero avere lo stesso nome. Inoltre potrebbe creare problemi in termini di mantenimento a lungo termine del progetto perchè potrebbe essere difficile capire dove e come è stato aggiunto questo metodo.

Adesso che abbiamo visto come funzionano i Prototypes possiamo passare ad un'introduzione base della Programmazione ad Oggetti oppure OOP.

Introduzione alla Programmazione ad Oggetti

Quando si parla di programmazione ad oggetti s'introducono tanti concetti: Classi, Constructor Functions, Extends, la keyword "new", la keyword "super" e così via. Tutti questi concetti servono tutti per lo stesso scopo: organizzare il nostro codice in modo da avere una struttura più pulita e mantenibile possibile.

In che modo organizziamo il nostro codice? Creando delle classi che rappresentano delle entità a cui facciamo riferimento. Es. "Persona", "Auto", "Animale", "Ristorante", ecc.

Quando si definisce una classe si sta creando una sorta di modello per una certa entità. Questo modello ci aiuta a definire le caratteristiche di questa entità e a definire le azioni che questa entità può compiere.

Quindi se per esempio stessimo definendo una classe che rappresenta una "Persona" potremmo definire le seguenti caratteristiche:

  • nome
  • cognome
  • età
  • sesso
  • altezza
  • peso
  • colore dei capelli
  • colore degli occhi
  • ecc.

E potremmo definire le seguenti azioni:

  • mangiare
  • dormire
  • lavorare
  • studiare
  • andare in palestra
  • ecc.

Queste sono caratteristiche ed azioni che tutte le persone hanno in comune. Ovviamente ogni persona ha delle caratteristiche e delle azioni che sono uniche e che non sono presenti in tutte le persone ma queste caratteristiche e azioni sono quelle che sono comuni a tutte le persone.

Factory Functions

Le factory function sono semplicemente funzioni che restituiscono un oggetto.

Questo oggetto può essere utilizzato come un'istanza di una classe, ma viene creato dalla funzione invece che da una classe. Le factory function sono spesso utilizzate per creare oggetti che condividono proprietà e metodi comuni, ma che non sono necessariamente istanze della stessa classe.

Ecco un esempio di factory function che crea oggetti Persona:

function creaPersona(nome, cognome, eta) {
  return {
    nome: nome,
    cognome: cognome,
    eta: eta,
    saluta: function() {
      console.log("Ciao, mi chiamo " + this.nome + " " + this.cognome + ".");
    }
  };
}

const persona1 = creaPersona("Mario", "Rossi", 30);
const persona2 = creaPersona("Luigi", "Verdi", 25);

persona1.saluta(); // Output: "Ciao, mi chiamo Mario Rossi."
persona2.saluta(); // Output: "Ciao, mi chiamo Luigi Verdi."

In questo esempio, la funzione creaPersona restituisce un oggetto che rappresenta una persona. L'oggetto ha proprietà come nome, cognome e eta, nonché un metodo saluta che fa il console log di un messaggio di saluto.

Constructor Functions

Le constructor functions risolvono lo stesso problema che le factory functions risolvono e di solito sono considerate una pratica migliore.

Hai notato come qualche volta abbiamo usato la sintassi new per creare un oggetto? Ad esempio possiamo creare un Array con la sintassi new Array().

Ci sono molteplici motivi per cui le constructor functions sono preferite rispetto alle factory. Vediamo alcuni di questi motivi.

La sintassi new è più chiara rispetto alla sintassi return. Quando si legge il codice che usa la sintassi new è più chiaro che si sta creando un nuovo oggetto.

Le factory functions duplicano per ogni oggetto creato le funzioni definite al suo interno. Per esempio, la funzione saluta per l'entità Persona viene duplicata per ogni persona creata. A noi non serve una funzione unica per ogni persona ma la stessa funzione che sia disponibile per ogni persona creata. Le constructor functions risolvono anche questo problema.

Andiamo a vedere un esempio di constructor function che crea oggetti Persona:

function Persona(nome, cognome, eta) {
  this.nome = nome;
  this.cognome = cognome;
  this.eta = eta;
}

Persona.prototype.saluta = function() {
  console.log("Ciao, mi chiamo " + this.nome + " " + this.cognome + ".");
};

const persona1 = new Persona("Mario", "Rossi", 30);
const persona2 = new Persona("Luigi", "Verdi", 25);

persona1.saluta(); // Output: "Ciao, mi chiamo Mario Rossi."
persona2.saluta(); // Output: "Ciao, mi chiamo Luigi Verdi."

In questo esempio, la funzione Persona è una constructor function che crea oggetti Persona. L'oggetto ha proprietà come nome, cognome e eta, nonché un metodo saluta che fa il console log di un messaggio di saluto.

Come? In realtà lo fa in modo molto simile a come la factory function fa. Quando utilizziamo la keyword new prima di chiamare la funzione Persona, JavaScript esegue le seguenti operazioni:

  • Crea un nuovo oggetto vuoto. // var this = {}
  • Assegna il valore this alla proprietà __proto__ dell'oggetto creato. // this.__proto__ = Persona.prototype
  • Esegui la funzione Persona passando il nuovo oggetto come valore this. // Persona.call(this, "Mario", "Rossi", 30)
  • Restituisci il nuovo oggetto. // return this

In questo modo this si riferirà all'oggetto che viene creato e non alla funzione Persona. Un'altra cosa importante da notare è che la funzione saluta viene definita direttamente sul prototype di Persona anzichè all'interno della funzione Persona il che ci permette di evitare una duplicazione della funzione saluta per ogni oggetto che viene creato.

Se adesso faremo un cambiamento alla funzione saluta lo potremo fare direttamente alla funzione che viene definita nel prototype e beneficiare del cambiamento per tutti gli oggetti Persona creati.

Questa è la magia che sta dietro la keyword new e le constructor functions :)

Adesso che abbiamo visto come creare oggetti con le constructor functions, posso introdurti ad un concetto che rende le cose più facili da leggere e gestire: la keyword class.

Classi

Come hai potuto notare, le constructor functions definiscono l'oggetto Persona. Poi per definire una funzione abbiamo dovuto utilizzare il prototype di Persona. Che cosa succede se volessimo definire un'altra funzione? Dobbiamo sempre far riferimento al prototype. E tutte queste funzioni che definiamo possono confondersi con altre funzioni che potrebbero rifersi ad altri oggetti. Per esempio, proviamo a definire due funzioni per Persona e una funzione per Cane:

function Persona(nome, cognome, eta) {
  this.nome = nome;
  this.cognome = cognome;
  this.eta = eta;
}

function Cane(nome, razza) {
  this.nome = nome;
  this.razza = razza;
}

Persona.prototype.saluta = function() {
  console.log("Ciao, mi chiamo " + this.nome + " " + this.cognome + ".");
};

Cane.prototype.abbaia = function() {
  console.log("Woof, woof!");
};

Persona.prototype.salutaInglese = function() {
  console.log("Hello, my name is " + this.nome + " " + this.cognome + ".");
};

Come vedi la prima e la terza funzione si riferiscono a Persona mentre la seconda si riferisce a Cane. Il codice è funzionante ma non è molto chiaro. Come possiamo fare per rendere più chiaro il codice? Possiamo utilizzare le classi.

Infatti le classi non cambiano la logica ma semplicemente la rendono più chiara e facile da leggere.

Vediamo come possiamo definire Persona e Cane utilizzando le classi:

class Persona {
  constructor(nome, cognome, eta) {
    this.nome = nome;
    this.cognome = cognome;
    this.eta = eta;
  }

  saluta() {
    console.log("Ciao, mi chiamo " + this.nome + " " + this.cognome + ".");
  }

  salutaInglese() {
    console.log("Hello, my name is " + this.nome + " " + this.cognome + ".");
  }
}

class Cane {
  constructor(nome, razza) {
    this.nome = nome;
    this.razza = razza;
  }

  abbaia() {
    console.log("Woof, woof!");
  }
}

const persona1 = new Persona("Mario", "Rossi", 30);
const persona2 = new Persona("Luigi", "Verdi", 25);

persona1.saluta(); // Output: "Ciao, mi chiamo Mario Rossi."
persona2.saluta(); // Output: "Ciao, mi chiamo Luigi Verdi."

const cane1 = new Cane("Fido", "Pastore Tedesco");
const cane2 = new Cane("Rex", "Pastore Tedesco");

cane1.abbaia(); // Output: "Woof, woof!"
cane2.abbaia(); // Output: "Woof, woof!"

Ora questo codice può spaventare all'inizio ma non preoccuparti, vedremo che è molto più semplice di quanto sembri.

La parte nuova è questa funzione constructor. All'interno del constructor definiamo le proprietà dell'oggetto. In questo caso nome, cognome e eta per Persona e nome e razza per Cane.

Le funzioni sono definite all'interno delle classi. Le funzioni saluta e salutaInglese sono definite all'interno della classe Persona mentre la funzione abbaia è definita all'interno della classe Cane.

Inoltre per le funzioni definite all'interno di una classe non abbiamo bisogno di utilizzare la keyword function.

Cosí possiamo facilmente vedere un inizio ed una fine di una classe e facilmente sapere quali sono tutte le proprietà e le funzioni che appartengono ad un oggetto.

Possiamo creare oggetti Persona o Cane ed utilizzarli esattamente allo stesso modo della constructor function che abbiamo visto precedentemente.

Quindi le classi sono solo un altro modo per definire oggetti e sono il modo che ad oggi troverai utilizzato più spesso.

Extends e Super - Ereditarietà

Entrambe le keywords extends e super sono utilizzate per creare una relazione di ereditarietà tra classi.

Per farti un esempio, immagina di avere una classe Lavoratore e una classe Studente ed entrambe hanno una funzione saluta:

class Lavoratore {
  constructor(nome, cognome, eta) {
    this.nome = nome;
    this.cognome = cognome;
    this.eta = eta;
  }

  saluta() {
    console.log("Ciao, mi chiamo " + this.nome + " " + this.cognome + ".");
  }
}

class Studente {
  constructor(nome, cognome, eta) {
    this.nome = nome;
    this.cognome = cognome;
    this.eta = eta;
  }

  saluta() {
    console.log("Ciao, mi chiamo " + this.nome + " " + this.cognome + ".");
  }
}

const lavoratore = new Lavoratore("Mario", "Rossi", 30);
const studente = new Studente("Luigi", "Verdi", 25);

lavoratore.saluta(); // Output: "Ciao, mi chiamo Mario Rossi."
studente.saluta(); // Output: "Ciao, mi chiamo Luigi Verdi."

Come vedi queste due classi condividono un bel pò di logica. Ad esempio hanno la funzione constructor con le stesse proprietà ed hanno la stessa funzione saluta. Questo perchè sia uno Studente che un Lavoratore in un contesto generale sono persone, hanno un nome, un cognome, un'età e possono salutare.

Aggiungiamo loro una funzione che li differenzia:

class Lavoratore {
  constructor(nome, cognome, eta) {
    this.nome = nome;
    this.cognome = cognome;
    this.eta = eta;
  }

  saluta() {
    console.log("Ciao, mi chiamo " + this.nome + " " + this.cognome + ".");
  }

  timbraIngresso() {
    console.log("Ho timbrato l'ingresso...");
  }
}

class Studente {
  constructor(nome, cognome, eta) {
    this.nome = nome;
    this.cognome = cognome;
    this.eta = eta;
  }

  saluta() {
    console.log("Ciao, mi chiamo " + this.nome + " " + this.cognome + ".");
  }

  prendiAppunti() {
    console.log("Sto prendendo appunti...");
  }
}

const lavoratore = new Lavoratore("Mario", "Rossi", 30);
const studente = new Studente("Luigi", "Verdi", 25);

lavoratore.saluta(); // Output: "Ciao, mi chiamo Mario Rossi."
studente.saluta(); // Output: "Ciao, mi chiamo Luigi Verdi."

lavoratore.timbraIngresso(); // Output: "Ho timbrato l'ingresso..."
studente.prendiAppunti(); // Output: "Sto prendendo appunti..."

Adesso entrambi hanno una funzione specifica. Un lavoratore timbra l'ingresso per entrare in ufficio mentre uno studente prende appunti durante una lezione ad esempio.

Questo è un buon esempio di ereditarietà. Entrambe le classi hanno delle proprietà e delle funzioni in comune, ma hanno anche delle proprietà e delle funzioni specifiche. Quindi come possiamo fare in modo che la classe Studente e la classe Lavoratore abbiano le proprietà e le funzioni in comune senza duplicare la logica due volte?

Qui entra in gioco la keyword extends che ci permetterà di creare una relazione tra le due classi.

Entrambe le entità, il Programmatore e lo Studente sono delle persone quindi potremmo creare una nuova entità Persona e far ereditare le classi Programmatore e Studente da Persona:

class Persona {
  constructor(nome, cognome, eta) {
    this.nome = nome;
    this.cognome = cognome;
    this.eta = eta;
  }

  saluta() {
    console.log("Ciao, mi chiamo " + this.nome + " " + this.cognome + ".");
  }
}

class Lavoratore extends Persona {
  timbraIngresso() {
    console.log("Ho timbrato l'ingresso...");
  }
}

class Studente extends Persona {
  prendiAppunti() {
    console.log("Sto prendendo appunti...");
  }
}

const lavoratore = new Lavoratore("Mario", "Rossi", 30);
const studente = new Studente("Luigi", "Verdi", 25);

lavoratore.saluta(); // Output: "Ciao, mi chiamo Mario Rossi."
studente.saluta(); // Output: "Ciao, mi chiamo Luigi Verdi."

lavoratore.timbraIngresso(); // Output: "Ho timbrato l'ingresso..."
studente.prendiAppunti(); // Output: "Sto prendendo appunti..."

In questo modo sia il Lavoratore che lo Studente hanno la funzione saluta e le proprietà nome, cognome ed eta ereditate dalla classe Persona senza che dobbiamo ripeterle due volte. Interessante, no? 🤓

Ora andiamo a vedere la parola super cosa ci permette di fare.

Se per esempio volessimo aggiungere una proprietà specifica alla classe Studente e evitare che questa proprietà venga ereditata anche dalla classe Persona potremmo fare cosí:

class Studente extends Persona {
  constructor(nome, cognome, eta, materia) {
    super(nome, cognome, eta);
    this.materia = materia;
  }

  prendiAppunti() {
    console.log("Sto prendendo appunti...");
  }
}

const studente = new Studente("Luigi", "Verdi", 25, "Matematica");

studente.saluta(); // Output: "Ciao, mi chiamo Luigi Verdi."
studente.prendiAppunti(); // Output: "Sto prendendo appunti..."
console.log(studente.materia); // Output: "Matematica"

In questo caso abbiamo usato la keyword super per chiamare il costruttore della classe Persona e passargli i parametri nome, cognome e eta. Questo ci permette di ereditare le proprietà nome, cognome e eta dalla classe Persona senza doverle ripetere due volte.

Se non avessimo usato la keyword super avremmo dovuto riscrivere interamente il constructor cosí:

class Studente extends Persona {
  constructor(nome, cognome, eta, materia) {
    this.nome = nome;
    this.cognome = cognome;
    this.eta = eta;
    this.materia = materia;
  }

  prendiAppunti() {
    console.log("Sto prendendo appunti...");
  }
}

E come puoi notare questa è una ripetizione di codice. Più codice significa più possibilità di errori e più codice da scrivere e da mantenere.

Nella programmazione viene sempre ricordato che il codice deve essere DRY (Don't Repeat Yourself) e questo è un ottimo esempio di come non dobbiamo ripetere codice. Ovvero non bisogna ripetere codice che è già stato scritto in precedenza.

E se non avessimo usato la keyword super e avessimo semplicemente scritto:

class Studente extends Persona {
  constructor(nome, cognome, eta, materia) {
    this.materia = materia;
  }

  prendiAppunti() {
    console.log("Sto prendendo appunti...");
  }
}

const studente = new Studente("Luigi", "Verdi", 25, "Matematica");

studente.saluta(); // Output: "Ciao, mi chiamo undefined undefined."

Avremmo ottenuto undefined perchè non avremmo chiamato il constructor della classe Persona e quindi le proprietà nome, cognome e eta non sarebbero state definite (undefined).

Quindi la soluzione quando si sta cercando di aggiungere delle proprietà specifiche ad un oggetto che eredita il constructor da un altro oggetto è quello dell'utilizzo della keyowrd super.

Conclusione

Questa è stata senza dubbio la classe più complessa finora del corso. Abbiamo visto una serie di argomenti complessi come il concetto di async/await, le callbacks, le promises, le APIs, la gestione di errori, il concetto di libreria esterna, le classi, l'ereditarietà. È stato senza dubbio un capitolo molto intenso infatti voglio tranquillizzarti che tutti questi concetti li rivedremo e soprattutto li utilizzeremo spesso nei prossimi capitoli in cui avrai modo di chiarire e solidificare questi concetti.

Voglio congratularmi per aver raggiunto questo punto del corso. Hai imparato tantissimo e sei pronto per affrontare la parte Backend del corso! 🎉