Articolo originale: The JavaScript Beginner's Handbook (2020 Edition) di Flavio Copes

Tradotto e adattato da: Dario Di Cillo

JavaScript è uno dei linguaggi di programmazione più popolari al mondo e rappresenta certamente un'ottima scelta come primo linguaggio per imparare a programmare.

Utilizziamo JavaScript principalmente per creare

  • siti web
  • applicazioni web
  • Applicazioni server-side usando Node.js

ma JavaScript non si limita a queste cose e può anche essere usato per

  • creare applicazioni per dispositivi mobili tramite strumenti come React Native
  • creare programmi per microcontrollori and the internet of things
  • creare applicazioni per smartwatch

In pratica, può essere usato per qualsiasi cosa. È così popolare che tutto ciò di nuovo che emerge avrà un qualche tipo di integrazione di JavaScript a un certo punto del suo sviluppo.

JavaScript è un linguaggio di programmazione:

  • ad alto livello: fornisce astrazioni che ti permettono di ignorare i dettagli della macchina durante l'esecuzione. Gestisce automaticamente la memoria tramite un garbage collector, quindi puoi concentrarti sul codice, senza preoccuparti troppo della gestione della memoria come in altri linguaggio tra cui C, e inoltre è dotato di svariati costrutti che ti permettono di aver a che fare con variabili e oggetti estremamente importanti.  
  • dinamico: in opposizione ai linguaggi di programmazione statici, un linguaggio dinamico esegue nel run-time molti processi che un linguaggio statico svolge durante il compile-time. Questo si riflette in svariati pro e contro e da ciò derivano caratteristiche di notevole importanza tra cui la tipizzazione dinamica, il binding dinamico, la riflessione, la programmazione funzionale, l'alterazione del run-time di un oggetto, la chiusura e molto altro. Non preoccuparti se non hai familiarità con questi concetti - dopo aver letto questa guida li conoscerai tutti.
  • dinamicamente tipizzato: il tipo di dato contenuto in una variabile non è fissato, quindi puoi riassegnare a una variabile qualsiasi tipo di dato, ad esempio, un intero a una variabile che contiene una stringa.
  • debolmente tipizzato: in opposizione alla tipizzazione forte, nei linguaggi debolmente tipizzati il tipo di oggetto non è fisso, offrendo una maggiore flessibilità impedendo l'uso di type safety e type checking (fornito da TypeScript - che è sviluppato a partire da JavaScript).
  • interpretato: è comunemente conosciuto come linguaggio interpretato, che vuol dire che non necessita di una fase di compilazione prima dell'esecuzione del programma, al contrario di C, Java o Go per esempio. In pratica, per una questione di performance, i browser compilano JavaScript prima di eseguirlo - senza bisogno di nessun un altro step.
  • multi-paradigma: il linguaggio non è vincolato da nessun tipo particolare di paradigma di programmazione, al contrario di Java che, ad esempio, impone l'uso della programmazione orientata agli oggetti, oppure C che obbliga alla programmazione imperativa. Puoi scrivere in JavaScript utilizzando un paradigma orientato agli oggetti usando prototipi e le nuove classi di sintassi (come ES6). Puoi scrivere in JavaScript in uno stile di programmazione funzionale, con le sue funzioni di prima classe o anche in uno stile imperativo (come in C).

Nel caso te lo stia chiedendo, JavaScript non ha niente a che fare con Java, la somiglianza è soltanto legata alla scelta del nome e dobbiamo conviverci.

Indice del manuale

  1. Un po' di storia
  2. Solo JavaScript
  3. Una breve introduzione alla sintassi di JavaScript
  4. Punto e virgola
  5. Valori
  6. Variabili
  7. Tipi
  8. Espressioni
  9. Operatori
  10. Ordine di priorità
  11. Operatori di confronto
  12. Condizionali
  13. Array
  14. Stringhe
  15. Loop
  16. Funzioni
  17. Funzioni freccia
  18. Oggetti
  19. Proprietà degli oggetti
  20. Metodi per gli oggetti
  21. Classi
  22. Ereditarietà
  23. Programmazione asincrona e callback
  24. Promise
  25. Async e Await
  26. Scope di una variabile
  27. Conclusione

Un po' di storia

JavaScript è stato creato nel 1995 ed è arrivato molto lontano, nonostante le sue umili origini.

È stato il primo linguaggio di scripting ad essere supportato dagli stessi browser, e grazie a ciò è diventato estremamente competitivo rispetto agli altri linguaggi, essendo tutt'ora l'unico linguaggio di scripting che possiamo usare per creare applicazioni web.

Altri linguaggi esistono, ma devono essere compilati in JavaScript - o più recentemente in WebAssembly, ma questa è un'altra storia.

Ai suoi inizi, JavaScript non era neanche lontanamente importante come lo è oggi ed era principalmente usato per animazione di fantasia e per quella meraviglia conosciuta al tempo come HTML dinamico.

Con le crescenti necessità che le piattaforme web richiedevano (e continuano a richiedere), JavaScript aveva la responsabilità di crescere per andare incontro ai bisogni di uno dei sistemi più utilizzati al mondo.

JavaScript anche oggi viene ampiamente utilizzato fuori dal browser. L'ascesa di Node.js negli ultimi anni ha sbloccato lo sviluppo backend, una volta dominio di Java, Ruby, Python, PHP e dei più tradizionali linguaggi server-side.

JavaScript è anche un linguaggio utilizzato per database e molte altre applicazioni ed è anche utilizzabile per lo sviluppo di applicazioni integrate, app mobili, app TV e molto altro. Quello che è nato come un linguaggio di nicchia all'interno del browser, adesso è il linguaggio più popolare al mondo.

Solo JavaScript

A volte è difficile separare JavaScript dalle caratteristiche dell'ambiente in cui viene utilizzato.

Ad esempio, il comando console.log() che puoi trovare in molti esempi di codice non è JavaScript, ma è parte della vasta libreria di API fornitaci dal browser.

Allo stesso modo, a volte può essere difficile separare sul server le caratteristiche del linguaggio JavaScript dalle API fornite da Node.js.

È una caratteristica data da React o Vue? O è "semplice JavaScript", o "vanilla JavaScript" come viene a volte chiamato?

In questo manuale, parlerò di JavaScript, il linguaggio, senza complicare il tuo processo di apprendimento con cose al di fuori di esso o fornite da ambienti esterni.

Una breve introduzione alla sintassi di JavaScript

In questa piccola introduzione voglio illustrarti 5 concetti:

  • spazi vuoti
  • case sensitivity
  • literal
  • identificatori
  • commenti

Spazi vuoti

JavaScript non considera gli spazi vuoti. Spazi e interruzioni di riga possono essere aggiunti in qualsiasi modo preferisci (almeno in teoria).

In pratica, tenderai a seguire uno stile ben definito, attenendoti a quello comunemente utilizzato, applicandolo tramite un linter o uno strumento di stile come Prettier.

Ad esempio, io utilizzo sempre due spazi per ogni indentazione.

Case sensitivity

JavaScript è case sensitive (sensibile alle maiuscole) e una variabile chiamata something è diversa da Something.

La stessa cosa è valida per qualsiasi identificatore.

Literal

Definiamo un literal come un valore scritto nel codice sorgente, ad esempio, un numero, una stringa, un booleano o anche un costrutto avanzato come un Object Literal o un Array Literal:

5
'Test'
true
['a', 'b']
{color: 'red', shape: 'Rectangle'}

Identificatori

Un identificatore è una sequenza di caratteri che viene utilizzata per identificare una variabile, una funzione o un oggetto. Può iniziare con una lettera, il simbolo del dollaro $ o un underscore _ e contenere cifre. Utilizzando Unicode, una lettera può essere un qualsiasi carattere consentito, ad esempio una emoji.

Test
test
TEST
_test
Test1
$test

Il simbolo del dollaro è comunemente usato per fare riferimento a elementi DOM.

Alcuni nomi sono riservati per l'uso interno di JavaScript e non possiamo usarli come identificatori.

Commenti

I commenti sono una parte estremamente importante di ogni programma, in qualsiasi linguaggio di programmazione. Sono importanti perché ci permettono di inserire annotazioni e aggiungere informazioni importanti che altrimenti non sarebbero disponibili per altre persone (o per noi stessi) durante la lettura del codice.

In JavaScript, possiamo scrivere un commento su una riga singola usando //. Tutto ciò che è presente dopo // non viene considerato come codice dall'interprete di JavaScript.

Ad esempio:

// a comment
true // another comment

Un altro tipo di commento è quello multi-riga, che inizia con /* e finisce con */.

Tutto ciò che è compreso nel mezzo, non viene considerato come codice:

/* some kind
of 
comment 

*/

Punto e virgola

Ogni riga di un programma in JavaScript viene terminata facoltativamente usando il punto e virgola.

Facoltativamente perché l'interprete di JavaScript è abbastanza intelligente da introdurre il punto e virgola per te.

Nella maggior parte dei casi, puoi omettere del tutto il punto e virgola nei tuoi programmi senza neanche pensarci.

Questo è un aspetto piuttosto controverso e alcuni sviluppatori utilizzano sempre il punto e virgola, mentre altri non ne fanno mai uso, così avrai sempre a che fare con del codice in cui è presente e altro in cui viene omesso.

La mia preferenza personale è di evitare il punto e virgola, quindi negli esempi di questo manuale verrà omesso.

Valori

Una stringa hello è un valore.
Un numero come il 12 è un valore.

hello e 12 sono valori. string e number costituiscono il tipo di questi valori.

Il tipo è il genere di un valore, la sua categoria. In JavaScript abbiamo molti tipi di valori differenti, ognuno con le proprie caratteristiche e di cui parleremo più avanti nel dettaglio.

Quando abbiamo bisogno di avere un riferimento per un valore, lo assegniamo ad una variabile.
La variabile possiede un nome e il valore è ciò è contenuto nella variabile, quindi possiamo avere accesso al valore tramite il nome della variabile.

Variabili

Una variabile è un valore assegnato ad un identificatore, a cui ci si può riferire per utilizzare la variabile all'interno del programma.

Questo accade perché JavaScript è debolmente tipizzato, un concetto di cui sentirai parlare spesso.

Prima di poter essere utilizzata, una variabile deve essere dichiarata.

Ci sono due modi principali per dichiarare le variabili. Il primo è l'uso di const:

const a = 0

Il secondo è let:

let a = 0

Qual è la differenza?

const definisce un riferimento costante per un valore. Questo vuol dire che il riferimento non può essere cambiato, che non puoi riassegnargli un nuovo valore.

Invece, usando let puoi assegnargli un nuovo valore.

Ad esempio, non puoi fare questo:

const a = 0
a = 1

In questo caso, otterrai l'errore: TypeError: Assignment to constant variable..

Al contrario, puoi farlo usandolet:

let a = 0
a = 1

const non significa "costante" nel senso che può avere in altri linguaggi come C. In particolare, non significa che il valore non può cambiare ma vuol dire che non può essere riassegnato. Se la variabile rimanda a un oggetto o un array (parleremo più avanti di oggetti e array) il contenuto dell'oggetto o dell'array può variare liberamente.

Le variabili definite con const devono essere inizializzate nel momento della dichiarazione:

const a = 0

Quelle definite tramite let possono essere inizializzate in un secondo momento:

let a
a = 0

Puoi dichiarare variabili multiple con una sola istruzione:

const a = 1, b = 2
let c = 1, d = 2

Ma non puoi ridichiarare la stessa variabile più di una volta:

let a = 1
let a = 2

O otterrai l'errore "duplicate declaration".

Il mio consiglio è di utilizzare sempre const e utilizzare let  soltanto quando sai che avrai bisogno di riassegnare il valore a una variabile, perché meno potere ha il tuo codice e meglio è. Un valore che non può essere riassegnato costituisce una potenziale fonte di bug in meno.

Dopo aver visto come funzionano const e let, vorrei parlare di var.

Prima del 2015, var era l'unico modo per dichiarare una variabile in JavaScript. Oggi, un moderno codebase conterrà con ogni probabilità soltanto const e let. Ci sono delle differenze sostanziali che ho esaminato nel dettaglio in questo post, ma se sei alle prime armi, non devi preoccupartene. Puoi utilizzare tranquillamente const e let.

Tipi

Le variabili in JavaScript non sono associate a un tipo particolare (sono untyped).

Una volta assegnato un valore di qualche tipo a una variabile, puoi riassegnare la variabile per contenere un valore di qualsiasi altro tipo senza problemi.

In JavaScript esistono principalmente due tipi: primitivi e oggetti.

Primitivi

I tipi primitivi sono:

  • numeri
  • stringhe
  • booleani
  • simboli

E due tipi speciali: null e undefined.

Oggetti

Qualsiasi valore che non è primitivo (una stringa, un numero, un booleano, null o undefined) è un oggetto.

Gli oggetti hanno proprietà e anche metodi che possono agire su di esse.

Parleremo ancora di oggetti più tardi.

Espressioni

Un'espressione è una singola unità di codice JavaScript che il motore JavaScript può valutare per restituire un valore.

Le espressioni hanno una complessità variabile.

Iniziamo con le più semplici, chiamate espressioni primarie:

2
0.02
'something'
true
false
this //the current scope
undefined
i //where i is a variable or a constant

Le espressioni aritmetiche sono espressioni che prendono una variabile e un operatore (o più operatori a breve) e restituiscono un numero:

1 / 2
i++
i -= 2
i * 2

Le espressioni possono anche restituire delle stringhe:

'A ' + 'string'

Le espressioni logiche fanno uso di operatori logici e restituiscono un valore booleano:

a && b
a || b
!a

Espressioni più avanzate includono oggetti, funzioni e array, e le introdurremo più avanti.

Operatori

Gli operatori ti permettono di ottenere espressioni complesse combinando due espressioni semplici.

Possiamo classificare gli operatori in base agli operandi su cui agiscono. Alcuni operatori lavorano con 1 operando, ma la maggior parte ne usa 2, mentre solo un operatore lavora con 3 operandi.

In questa prima introduzione agli operatori, inizieremo a parlare di quelli che ci sono più familiari: gli operatori con 2 operandi.

Ne ho già introdotto uno, parlando delle variabili: l'operatore di assegnazione =, che viene utilizzato per assegnare un valore a una variabile.

let b = 2

Adesso, occupiamoci di un altro set di operatori binari con cui hai familiarità dalla matematica di base.

L'operatore di addizione (+)

const three = 1 + 2
const four = three + 1

L'operatore + agisce anche sulle stringhe, concatenandole, quindi fai attenzione:

const three = 1 + 2
three + 1 // 4
'three' + 1 // three1

L'operatore di sottrazione (-)

const two = 4 - 2

L'operatore di divisione (/)

Restituisce il quoziente tra il primo operando e il secondo:

const result = 20 / 5 //result === 4
const result = 20 / 7 //result === 2.857142857142857

Se dividi per zero, JavaScript non ti segnala nessun errore ma restituisce il valore Infinity (o -Infinity se il valore è negativo).

1 / 0 //Infinity
-1 / 0 //-Infinity

L'operatore modulo (%)

Questo operatore restituisce il resto di una divisione e può essere molto utile in molte applicazioni:

const result = 20 % 5 //result === 0
const result = 20 % 7 //result === 6

Il resto di una divisione per zero è sempre NaN, un valore speciale che vuol dire "non un numero" ("Not a Number"):

1 % 0 //NaN
-1 % 0 //NaN

L'operatore di moltiplicazione (*)

Moltiplica due numeri:

1 * 2 //2
-1 * 2 //-2

L'operatore di elevamento a potenza (**)

Eleva il primo operando alla potenza del secondo operando:

1 ** 2 //1
2 ** 1 //2
2 ** 2 //4
2 ** 8 //256
8 ** 2 //64

Ordine di priorità

Ogni istruzione complessa con operatori multipli nella stessa riga introduce un problema di priorità, ad esempio:

let a = 1 * 2 + 5 / 2 % 2

Il risultato è 2.5, ma perché?

Quali operazioni vengono eseguite prima e in un momento successivo?

Alcune operazioni hanno precedenza su altre. Le regole che stabiliscono la priorità sono elencate in questa tabella:

OPERATORE DESCRIZIONE
* / % moltiplicazione/divisione
+ - addizione/sottrazione
= assegnazione

Le operazioni dello stesso livello (come + e-) sono eseguite nell'ordine in cui vengono incontrate, da sinistra a destra.

Seguendo queste regole, l'esempio precedente si risolve in questo modo:

let a = 1 * 2 + 5 / 2 % 2
let a = 2 + 5 / 2 % 2
let a = 2 + 2.5 % 2
let a = 2 + 0.5
let a = 2.5

Operatori di confronto

Dopo l'operatore di assegnazione e gli operatori aritmetici, il terzo set di operatori che voglio introdurre è quello degli operatori di confronto.

Puoi utilizzare i seguenti operatori per comparare due numeri o due stringhe.

Gli operatori di confronto restituiscono sempre un valore booleano, cioè true o false.

Ecco gli operatori di confronto di disuguaglianza:

  • < significa "minore"
  • <= significa "minore o uguale"
  • > significa "maggiore"
  • >= significa "maggiore o uguale"

Esempio:

let a = 2
a >= 1 //true

In aggiunta a questi, abbiamo 4 operatori di uguaglianza. Accettano due valori e restituiscono un booleano:

  • === verifica l'uguaglianza
  • !== verifica la disuguaglianza

In JavaScript sono anche disponibili == e!=, ma suggerisco caldamente di utilizzare soltanto === e !== per evitare dei subdoli problemi.

Condizionali

Dopo aver affrontato gli operatori di confronto, possiamo passare ai condizionali.

L'istruzione if viene utilizzata per far prendere al programma una strada o un'altra, a seconda del risultato della valutazione di un'espressione.

Questo è l'esempio più semplice, che viene sempre eseguito:

if (true) {
  //do something
}

Al contrario, questo non viene mai eseguito:

if (false) {
  //do something (? never ?)
}

Il condizionale verifica l'espressione che gli fornisci come vera o falsa. Dalla valutazione di un numero si ottiene sempre true, a meno che sia zero. Le stringhe vengono sempre valutate come true, eccetto le stringhe vuote. Queste sono le regole generali di come le espressioni vengono valutate in un contesto booleano.

Hai notato le parentesi graffe? Ciò che è compreso al loro interno è chiamato blocco e viene utilizzato per raggruppare un insieme di varie istruzioni

Un blocco può essere inserito in qualsiasi posto puoi inserire una singola istruzione. Se hai una singola istruzione da eseguire dopo il condizionale, puoi omettere il blocco scrivendo solo l'istruzione:

if (true) doSomething()

Ma mi piace utilizzare sempre le parentesi graffe per essere più chiaro.

Dopo l'istruzione if, puoi aggiungere una seconda parte: l'istruzione else, che verrà eseguita se la condizione if risulta falsa.

if (true) {
  //do something
} else {
  //do something else
}

Dato che else accetta un'espressione, puoi annidare al suo interno un'altra istruzione if/else:

if (a === true) {
  //do something
} else if (b === true) {
  //do something else
} else {
  //fallback
}

Array

Un array è un insieme di elementi.

Gli array in JavaScript non costituiscono un tipo per proprio conto.

Gli array sono oggetti.

Possiamo inizializzare un array vuoto in 2 modi diversi:

const a = []
const a = Array()

Il primo è utilizzando la sintassi letterale array, mentre il secondo è tramite la funzione integrata Array().

Puoi pre-inserire elementi nell'array tramite questa sintassi:

const a = [1, 2, 3]
const a = Array.of(1, 2, 3)

Un array può contenere qualsiasi valore, anche valori di diverso tipo:

const a = [1, 'Flavio', ['a', 'b']]

Siccome possiamo aggiungere un array all'interno di un altro array, possiamo creare array multi-dimensionali, che hanno applicazioni molto utili (ad esempio le matrici):

const matrix = [
  [1, 2, 3],
  [4, 5, 6],
  [7, 8, 9]
]

matrix[0][0] //1
matrix[2][0] //7

Puoi accedere a ogni elemento di un array facendo riferimento al suo indice, partendo da zero:

a[0] //1
a[1] //2
a[2] //3

Puoi inizializzare un nuovo array con un set di valori utilizzando questa sintassi, che prima inizializza un array di 12 elementi e poi inserisce il numero 0 per ciascun elemento:

Array(12).fill(0)

Puoi ottenere il numero di elementi di un array verificando la proprietà length:

const a = [1, 2, 3]
a.length //3

Nota che puoi impostare la lunghezza di un array. Se assegni a un array un numero più grande della sua attuale capacità non accade nulla. Se invece gli assegni un numero più piccolo, l'array viene troncato in quella posizione:

const a = [1, 2, 3]
a //[ 1, 2, 3 ]
a.length = 2
a //[ 1, 2 ]

Come aggiungere un elemento a un array

Possiamo aggiungere un elemento alla fine di un array usando il metodo push():

a.push(4)

O aggiungere un elemento all'inizio di un array con il metodo unshift():

a.unshift(0)
a.unshift(-2, -1)

Come rimuovere un elemento da un array

Possiamo rimuovere un elemento dalla fine di un array usando il metodo pop():

a.pop()

O rimuovere un elemento dall'inizio di un array con il metodo shift():

a.shift()

Come unire due o più array

Puoi unire più array utilizzando il metodo concat():

const a = [1, 2]
const b = [3, 4]
const c = a.concat(b) //[1,2,3,4]
a //[1,2]
b //[3,4]

Puoi anche utilizzare l'operatore spread (...) in questo modo:

const a = [1, 2]
const b = [3, 4]
const c = [...a, ...b]
c //[1,2,3,4]

Come trovare un valore specifico in un array

Puoi usare il metodo find() per trovare un elemento in un array:

a.find((element, index, array) => {
  //return true or false
})

Questo metodo dà come valore di ritorno il primo elemento che restituisce true, oppure undefined se l'elemento non viene trovato.

Una sintassi comune è:

a.find(x => x.id === my_id)

La riga di codice qui sopra restituisce il primo elemento dell'array che ha id === my_id.

findIndex() funziona in modo simile a find(), ma restituisce l'indice del primo elemento che corrisponde alla ricerca, oppure restituisce undefined, se non viene trovato:

a.findIndex((element, index, array) => {
  //return true or false
})

Un altro metodo è includes():

a.includes(value)

Restituisce true se a contiene value.

a.includes(value, i)

Restituisce true se a contiene value dopo la posizione i.

Stringhe

Una stringa è una sequenza di caratteri.

Può essere anche definita come stringa letterale, che è racchiusa tra virgolette singole o doppie:

'A string'
"Another string"

Personalmente preferisco sempre usare le virgolette singole e utilizzare le doppie soltanto in HTML per definire attributi.

Puoi assegnare una stringa a una variabile, in questo modo:

const name = 'Flavio'

Puoi determinare la lunghezza di una stringa usando la sua proprietà length:

'Flavio'.length //6
const name = 'Flavio'
name.length //6

Questa è una stringa vuota: '' e la sua lunghezza è 0:

''.length //0

Due stringhe possono essere unite con l'operatore +:

"A " + "string"

Puoi utilizzare l'operatore + per inserire delle variabili:

const name = 'Flavio'
"My name is " + name //My name is Flavio

Un altro modo di definire una stringa avviene tramite l'uso di template literal, definiti all'interno di due backtick (accento grave). Sono molto utili soprattutto per rendere più semplice la scrittura di stringhe su più righe. Con le virgolettte singole o doppie non è facile definire una stringa su più riga - c'è bisogno di utilizzare dei caratteri di escape.

Una volta aperto un template literal con il backtick, devi solo premere invio per creare una nuova riga, senza caratteri speciali:

const string = `Hey
this

string
is awesome!`

I template literal sono fantastici perché forniscono un modo semplice per inserire variabili ed espressioni all'interno di stringhe.

Puoi farlo usando la sintassi ${...}:

const v = 'test'
const string = `something ${v}` 
//something test

All'interno di ${} puoi inserire qualsiasi cosa, persino delle espressioni:

const string = `something ${1 + 2 + 3}`
const string2 = `something 
  ${foo() ? 'x' : 'y'}`

Loop

I loop sono una delle principali strutture di controllo di JavaScript.

Grazie a un loop è possibile automatizzare e ripetere l'esecuzione di un blocco di codice un numero qualsiasi di volte, anche indefinitamente.

JavaScript offre vari modi di eseguire iterazioni attraverso i loop.

Mi concentrerò su questi 3 modi:

  • loop while
  • loop for
  • loop for..of

while

Il loop while è la struttura più semplice che JavaScript ci offre.

Aggiungiamo una condizione dopo la keyword while e un blocco di codice che verrà eseguito finché la condizione è true.

Esempio:

const list = ['a', 'b', 'c']
let i = 0
while (i < list.length) {
  console.log(list[i]) //value
  console.log(i) //index
  i = i + 1
}

Puoi interrompere il loop while usando la keyword break, in questo modo:

while (true) {
  if (somethingIsTrue) break
}

Se invece decidi che durante l'esecuzione del loop vuoi saltare dall'iterazione corrente alla successiva, puoi farlo usando il comando continue:

while (true) {
  if (somethingIsTrue) continue

  //do something else
}

I loop do..while sono molto simili ai loop while. La differenza sta nel fatto che la condizione viene valutata dopo l'esecuzione del blocco di codice.

Ciò significa che il blocco viene sempre eseguito almeno una volta.

Esempio:

const list = ['a', 'b', 'c']
let i = 0
do {
  console.log(list[i]) //value
  console.log(i) //index
  i = i + 1
} while (i < list.length)

for

La seconda importante struttura di iterazione in JavaScript è il loop for.

Utilizziamo la keyword for e aggiungiamo un set di 3 istruzioni: l'inizializzazione, la condizione e l'aggiornamento delle variabili.

Esempio:

const list = ['a', 'b', 'c']

for (let i = 0; i < list.length; i++) {
  console.log(list[i]) //value
  console.log(i) //index
}

Proprio come i loop while, puoi interrompere un loop for usando break o saltare all'iterazione successiva di un loop for tramite continue.

for...of

Questo loop è relativamente recente (è stato introdotto nel 2015) ed è una versione semplificata del loop for:

const list = ['a', 'b', 'c']

for (const value of list) {
  console.log(value) //value
}

Funzioni

In qualsiasi programma JavaScript che presenta una discreta complessità, tutto si svolge grazie a delle funzioni.

Le funzioni costituiscono il nucleo, la parte essenziale di JavaScript.

Cos'è una funzione?

Una funzione è un blocco di codice autonomo.

Ecco un esempio di dichiarazione di funzione:

function getData() {
  // do something
}

Una funzione può essere eseguita in qualsiasi momento desideri, richiamandola, in questo modo:

getData()

Una funzione può avere uno o più argomenti:

function getData() {
  //do something
}

function getData(color) {
  //do something
}

function getData(color, age) {
  //do something
}

Quando richiamiamo la funzione, forniamo dei parametri che fungono da argomenti:

function getData(color, age) {
  //do something
}

getData('green', 24)
getData('black')

Come puoi notare, durante la seconda chiamata della funzione, ho fornito soltanto il parametro black come argomento di color, senza passare nessun paramentro come argomento per age. In questo caso, age all'interno della funzione è undefined.

Possiamo verificare se un valore non è definito usando questo condizionale:

function getData(color, age) {
  //do something
  if (typeof age !== 'undefined') {
    //...
  }
}

typeof è un operatore unitario che ci permette di verificare il tipo di una variabile.

Puoi anche effettuare il controllo in questo modo:

function getData(color, age) {
  //do something
  if (age) {
    //...
  }
}

Ma il condizionale sarà falso anche se age è null, 0 o una stringa vuota.

Puoi impostare dei valori di default per i parametri, nel caso non vengano passati:

function getData(color = 'black', age = 25) {
  //do something
}

Puoi passare qualsiasi valore come parametro: numeri, stringhe, booleani, array, oggetti e anche funzioni.

Una funzione ha un valore di ritorno. Di default una funzione restituisce undefined, a meno che non aggiungi la keyword return con un valore:

function getData() {
  // do something
  return 'hi!'
}

Quando invochiamo la funzione, possiamo anche assegnare il valore di ritorno a una variabile:

function getData() {
  // do something
  return 'hi!'
}

let result = getData()

result adesso contiene una stringa con il valore hi!.

Puoi avere soltanto un valore di ritorno.

Per avere più valori, devi utilizzare un oggetti o un array, in questo modo:

function getData() {
  return ['Flavio', 37]
}

let [name, age] = getData()

Le funzioni possono essere definite all'interno di altre funzioni:

const getData = () => {
  const dosomething = () => {}
  dosomething()
  return 'test'
}

Le funzioni annidate non possono essere chiamate dall'esterno della funzione che le racchiude.

Puoi anche avere una funzione come valore di ritorno di una funzione.

Funzioni freccia

La funzioni freccia sono state introdotte di recente in JavaScript.

Vengono utilizzate molto spesso al posto delle funzioni "ordinarie", quelle che abbiamo appena descritto nel capitolo precedente. Troverai ovunque entrambe le forme.

Visivamente, permettono di scrivere funzioni con una sintassi più breve, da:

function getData() {
  //...
}

a

() => {
  //...
}

Ma considera che queste funzioni non hanno un nome.

Le funzioni freccia sono anonime e dobbiamo assegnarle a una variabile.

Possiamo assegnare una funzione ordinaria a una variabile in questo modo:

let getData = function getData() {
  //...
}

In questa operazione, possiamo rimuovere il nome dalla funzione:

let getData = function() {
  //...
}

e invocare la funzione tramite il nome della variabile:

let getData = function() {
  //...
}
getData()

Analogamente, possiamo fare la stessa cosa per le funzioni freccia:

let getData = () => {
  //...
}
getData()

Se il corpo della funzione contiene solo una singola istruzione, possiamo omettere le parentesi e scrivere tutto su una sola riga:

const getData = () => console.log('hi!')

I parametri vengono passati all'interno delle parentesi:

const getData = (param1, param2) => 
  console.log(param1, param2)

Se la funzione ha un parametro (soltanto uno), puoi omettere del tutto le parentesi:

const getData = param => console.log(param)

Le funzioni freccia ti permettono di avere un return implicito - i valori vengono restituiti senza bisogno di usare la keyword return.

Funziona quando abbiamo una istruzione su una riga nel corpo della funzione:

const getData = () => 'test'

getData() //'test'

Come nelle funzioni ordinarie, possiamo avere dei valori di default per i parametri nel caso in cui non vengano passati:

const getData = (color = 'black', 
                 age = 2) => {
  //do something
}

E come le funzioni regolari, abbiamo soltanto un valore di ritorno.

Le funzioni freccia possono anche contenere altre funzioni freccia o anche funzioni regolari.

Questi due tipi di funzioni sono molto simili, quindi ti potrai chiedere perché mai sono state introdotte le funzioni freccia. La differenza più consistente rispetto alle funzioni ordinarie è evidente quando vengono usate come metodi per oggetti. Affronteremo a breve questo aspetto.

Oggetti

Qualsiasi valore che non appartiene a un tipo primitivo (una stringa, un numero, un booleano, un simbolo, null o undefined) è un oggetto.

Ecco come definiamo un oggetto:

const car = {

}

Questa è la sintassi letterale di un oggetto, una delle cose migliori in JavaScript.

Puoi anche utilizzare la sintassi new Object:

const car = new Object()

Un'altra sintassi è Object.create():

const car = Object.create()

Puoi anche inizializzare un oggetto usando la keyword new prima della funzione con la lettera maiuscola. Questa funzione agisce come costruttore per un oggetto. Al suo interno, possiamo inizializzare gli argomenti come parametri, per impostare lo stato iniziale dell'oggetto:

function Car(brand, model) {
  this.brand = brand
  this.model = model
}

Inizializziamo un nuovo oggetto in questo modo:

const myCar = new Car('Ford', 'Fiesta')
myCar.brand //'Ford'
myCar.model //'Fiesta'

Gli argomenti degli oggetti sono sempre passati per riferimento.

Se assegni a una variabile lo stesso valore di un'altra, nel caso sia di tipo primitivo come un numero o una stringa, viene passato per valore:

Ad esempio:

let age = 36
let myAge = age
myAge = 37
age //36
const car = {
  color: 'blue'
}
const anotherCar = car
anotherCar.color = 'yellow'
car.color //'yellow'

Anche gli array e le funzioni, sotto sotto sono degli oggetti, quindi è molto importante capire come funzionano.

Proprietà degli oggetti

Gli oggetti hanno proprietà, che sono formate da un'etichetta associata a un valore.

Il valore di una proprietà può essere di qualsiasi tipo: può essere un array, una funzione o addirittura un oggetto, in quanto gli oggetti possono essere annidati in altri oggetti.

Questa è la sintassi letterale per gli oggetti che abbiamo visto nel capitolo precedente:

const car = {

}

Possiamo definire la proprietà color in questo modo:

const car = {
  color: 'blue'
}

Abbiamo l'oggetto car con una proprietà chiamata color, che ha valore blue.

Le etichette possono essere rappresentate da qualsiasi stringa, ma attenzione ai caratteri speciali - se vuoi includere un carattere non valido come nome di una variabile nel nome della proprietà, devi racchiuderlo tra virgolette:

const car = {
  color: 'blue',
  'the color': 'blue'
}

Tra i caratteri non validi per il nome di variabili ci sono spazi, trattini e altri caratteri speciali.

Come puoi vedere, quando abbiamo più proprietà, dobbiamo separarle con una virgola.

Possiamo recuperare il valore di una proprietà usando 2 sintassi diverse.

La prima è la dot notation:

car.color //'blue'

La seconda (che è la sola che possiamo utilizzare per le proprietà con nomi non validi) si avvale delle parentesi quadre:

car['the color'] //'blue'

Se provi ad accedere a una proprietà inesistente, otterrai il valore undefined:

car.brand //undefined

Come detto in precedenza, gli oggetti possono avere oggetti annidati come proprietà:

const car = {
  brand: {
    name: 'Ford'
  },
  color: 'blue'
}

In quest'esempio, puoi avere accesso al nome nome del brand usando:

car.brand.name

o

car['brand']['name']

Puoi impostare il valore di una proprietà quando definisci l'oggetto.

Ma puoi aggiornarlo in un qualsiasi momento:

const car = {
  color: 'blue'
}

car.color = 'yellow'
car['color'] = 'red'

E puoi anche aggiungere nuove proprietà a un oggetto:

car.model = 'Fiesta'

car.model //'Fiesta'

Considerando l'oggetto:

const car = {
  color: 'blue',
  brand: 'Ford'
}

Puoi eliminare una proprietà dell'oggetto tramite:

delete car.brand

Metodi per gli oggetti

Abbiamo parlato delle funzioni in un capitolo precedente.

Le funzioni possono essere assegnate alle proprietà di un oggetto e in questo caso vengono chiamate metodi.

In quest'esempio, la proprietà start ha una funzione assegnata e possiamo chiamarla usando la sintassi per le proprietà con il punto e le parentesi alla fine:

const car = {
  brand: 'Ford',
  model: 'Fiesta',
  start: function() {
    console.log('Started')
  }
}

car.start()

All'interno del metodo definito usando la sintassi function() {}, abbiamo accesso alle istanze dell'oggetto facendo riferimento a this.

Nel prossimo esempio, accediamo ai valori delle proprietà brand e model usando this.brand e this.model:

const car = {
  brand: 'Ford',
  model: 'Fiesta',
  start: function() {
    console.log(`Started 
      ${this.brand} ${this.model}`)
  }
}

car.start()

È importante notare la distinzione tra le funzioni ordinarie e le funzioni freccia - non possiamo usare this con le funzioni freccia:

const car = {
  brand: 'Ford',
  model: 'Fiesta',
  start: () => {
    console.log(`Started 
      ${this.brand} ${this.model}`) //not going to work
  }
}

car.start()

Questo accade perché le funzioni freccia non sono legate all'oggetto.

Questa è la ragione per cui le funzioni ordinarie vengono spesso usate come metodi per oggetti.

I metodi accettano parametri, come le funzioni ordinarie:

const car = {
  brand: 'Ford',
  model: 'Fiesta',
  goTo: function(destination) {
    console.log(`Going to ${destination}`)
  }
}

car.goTo('Rome')

Classi

Abbiamo parlato di oggetti, che sono una delle parti più interessanti di JavaScript.

In questo capitolo, saliremo di livello introducendo le classi.

Cosa sono le classi? Sono un modo per definire un pattern comune a più oggetti.

Consideriamo l'oggetto person:

const person = {
  name: 'Flavio'
}

Creiamo una classe chiamata Person (nota la P maiuscola, una convenzione usata per le classi), che possiede una proprietà name:

class Person {
  name
}

Da questa classe, inizializziamo un oggetto flavio, in questo modo:

const flavio = new Person()

flavio è detto istanza della classe Person.

Possiamo modificare il valore della proprietà name:

flavio.name = 'Flavio'

e accedervi tramite

flavio.name

come facciamo per le proprietà di un oggetto.

Le classi contengono proprietà, come name, e metodi.

I metodi vengono definiti in questo modo:

class Person {
  hello() {
    return 'Hello, I am Flavio'
  }
}

E possiamo invocare un metodo su un'istanza della classe:

class Person {
  hello() {
    return 'Hello, I am Flavio'
  }
}
const flavio = new Person()
flavio.hello()

Esiste un metodo particolare, chiamato constructor() che possiamo usare per inizializzare le proprietà di una classe quando creiamo una nuova istanza di un oggetto.

Funziona in questo modo:

class Person {
  constructor(name) {
    this.name = name
  }

  hello() {
    return 'Hello, I am ' + this.name + '.'
  }
}

Nota come utilizziamo this per accedere all'istanza dell'oggetto.

Ora possiamo creare una nuova istanza della classe, passare una stringa e una volta chiamata hello otterremo un messaggio personalizzato:

const flavio = new Person('flavio')
flavio.hello() //'Hello, I am flavio.'

Quando l'oggetto è inizializzato, il metodo constructor viene chiamato con qualsiasi parametro passato.

Di norma i metodi vengono definiti su un'istanza di un oggetto e non sulla classe.

Possiamo definire un metodo come static per far sì che possa essere eseguito sulla classe:

class Person {
  static genericHello() {
    return 'Hello'
  }
}

Person.genericHello() //Hello

A volte può essere molto utile.

Ereditarietà

Una classe può estendere un'altra classe e gli oggetti inizializzati usando quella classe ereditano tutti i metodi di entrambe le classi.

Supponiamo di avere una classe Person:

class Person {
  hello() {
    return 'Hello, I am a Person'
  }
}

Definiamo una nuova classe, Programmer, che estende Person:

class Programmer extends Person {

}

Se creiamo un'istanza della classe Programmer, avrà accesso al metodo hello():

const flavio = new Programmer()
flavio.hello() //'Hello, I am a Person'

All'interno di una classe figlia, puoi far riferimento alla classe genitrice tramite super():

class Programmer extends Person {
  hello() {
    return super.hello() + 
      '. I am also a programmer.'
  }
}

const flavio = new Programmer()
flavio.hello()

Il programma qui sopra restituisce Hello, I am a Person. I am also a programmer..

Programmazione asincrona e callback

La maggior parte delle volte, il codice di JavaScript viene eseguito in maniera sincrona.

Ciò significa che viene eseguita una riga di codice e poi si passa alla successiva e così via.

Tutto accade come te lo puoi aspettare e nel modo in cui funziona la maggioranza dei linguaggi di programmazione.

A volte però, non è possibile attendere semplicemente l'esecuzione di una riga di codice per passare alla successiva..

Non si possono attendere 2 secondi per il caricamento di un file pesante e bloccare completamente il programma.

Non si può aspettare che una risorsa network venga scaricate prima di fare qualcos'altro.

JavaScript risolve questi problemi utilizzando i callback.

Uno degli esempi più semplici di come vengono impiegati i callback riguarda i timer. I timer non fanno parte di JavaScript, ma vengono offerti dal browser e da Node.js. Parliamo di uno dei timer che abbiamo a disposizione: setTimeout().

La funzione setTimeout() accetta due argomenti: una funzione e un numero. Il numero rappresenta i millisecondi che devono passare prima che la funzione venga eseguita.

Esempio:

setTimeout(() => {
  // esegue dopo 2 secondi
  console.log('inside the function')
}, 2000)

La funzione contenente la riga console.log('inside the function') verrà eseguita dopo 2 secondi.

Se aggiungi console.log('before') prima della funzione e console.log('after') dopo di essa:

console.log('before')
setTimeout(() => {
  // runs after 2 seconds
  console.log('inside the function')
}, 2000)
console.log('after')

Vedrai questo nella tua console:

before
after
inside the function

La funzione callback viene eseguita in modo asincrono.

Questo è un modello molto comune quando si lavora con file di sistema, network, eventi o il DOM nel browser.

Tutti questi aspetti non sono legati al "nucleo" di JavaScript, quindi non sono discussi in questo manuale, ma troverai molti esempi a riguardo nel mio altro manuale disponibile (in inglese) al sito https://flaviocopes.com.

Ecco come mettere in pratica i callback nel tuo codice.

Definiamo una funzione che accetta un parametro callback, che è una funzione.

Quando il codice è pronto per invocare il callback, lo invochiamo passandogli result:

const doSomething = callback => {
  //do things
  //do things
  const result = /* .. */
  callback(result)
}

Per invocare la funzione doSomething(), passiamo una funzione come parametro in questo modo:

doSomething(res => {
  console.log(res)
})

Promise

Le promise costituiscono un modo alternativo di gestire il codice asincrono.

Come abbiamo visto nel capitolo precedente, con un callback passiamo una funzione come argomento di un'altra funzione che verrà eseguita quando quest'ultima ha finito di processare.

In questo modo:

doSomething(res => {
  console.log(res)
})

Quando il codice di doSomething() finisce, richiama la funzione ricevuta come parametro:

const doSomething = callback => {
  //do things
  //do things
  const result = /* .. */
  callback(result)
}

Il problema principale di questo approccio è che se abbiamo bisogno di usare il risultato della funzione nel resto del nostro programma, tutto il nostro codice deve essere annidato all'interno del callback, e se abbiamo 2 o 3 callback entriamo in quello che viene di solito chiamato "inferno callback", con molti livelli di funzioni indentate in altre funzioni:

doSomething(result => {
  doSomethingElse(anotherResult => {
    doSomethingElseAgain(yetAnotherResult => {
      console.log(result)
    })
  }) 
})

Le promise sono un modo per aggirare questo problema.

Invece di scrivere:

doSomething(result => {
  console.log(result)
})

Invochiamo una funzione promise, in questo modo:

doSomething()
  .then(result => {
    console.log(result)
  })

Dopo aver chiamato la funzione, utilizziamo il metodo then().

L'indentazione non è rilevante ma la troverai spesso utilizzata per chiarezza.

È comune utilizzare il metodo catch() per gestire gli errori:

doSomething()
  .then(result => {
    console.log(result)
  })
  .catch(error => {
    console.log(error)
  })

Adesso, per essere in grado di utilizzare questa sintassi, l'esecuzione della funzione doSomething() deve essere un po' particolare, infatti deve utilizzare le promise API.

Invece di dichiararla come una normale funzione:

const doSomething = () => {
  
}

La dichiariamo come oggetto promise:

const doSomething = new Promise()

e passiamo una funzione al costruttore promise:

const doSomething = new Promise(() => {

})

Questa funzione riceve 2 parametri. Il primo è una funzione che chiamiamo per risolvere la promise, mentre il secondo è una funzione che chiamiamo per rigettarla.

const doSomething = new Promise(
  (resolve, reject) => {
    
})

Risolvere una promise vuol dire completarla con successo (chiamando di conseguenza il metodo then())

Rigettare una promise significa terminare con un errore (chiamando il metodo  catch()).

Ad esempio:

const doSomething = new Promise(
  (resolve, reject) => {
    //some code
    const success = /* ... */
    if (success) {
      resolve('ok')
    } else {
      reject('this error occurred')
    }
  }
)

Possiamo passare un parametro per risolvere e rigettare funzioni di qualsiasi tipo.

Async e await

Le funzioni async sono a un livello di astrazione superiore alle promise.

Una funzione async restituisce una promise, come in questo esempio:

const getData = () => {
  return new Promise((resolve, reject) => {
    setTimeout(() => 
      resolve('some data'), 2000)
  })
}

Il codice per usare questa funzione necessita della keyword await prima della funzione:

const data = await getData()

Facendo ciò, qualsiasi dato restituito dalla promise viene assegnato alla variabile data.

Nel nostro caso, il dato è la stringa "some data".

Dobbiamo avere una particolare attenzione: dovunque utilizziamo la keyword await, è necessario farlo all'interno di una funzione definita come async.

In questo modo:

const doSomething = async () => {
  const data = await getData()
  console.log(data)
}

Il duo async/await permette di avere un codice più pulito e un semplice modello mentale per lavorare con il codice asincrono.

Come puoi vedere nell'esempio qui sopra, il codice è molto semplice in confronto a quello contenente promise o callback.

E questo è un esempio molto semplice, ma i benefici più consistenti vengono fuori quando il codice cresce in complessità.

Come esempio, ecco come puoi ottenere una risorsa JSON usando la API fetch, e analizzarla usando le promise:

const getFirstUserData = () => {
  // get users list
  return fetch('/users.json') 
    // parse JSON
    .then(response => response.json()) 
    // pick first user
    .then(users => users[0]) 
    // get user data
    .then(user => 
      fetch(`/users/${user.name}`)) 
    // parse JSON
    .then(userResponse => response.json()) 
}

getFirstUserData()

Ed ecco la stessa operazione svolta usando await/async:

const getFirstUserData = async () => {
  // get users list
  const response = await fetch('/users.json') 
  // parse JSON
  const users = await response.json() 
  // pick first user
  const user = users[0] 
  // get user data
  const userResponse = 
    await fetch(`/users/${user.name}`)
  // parse JSON
  const userData = await user.json() 
  return userData
}

getFirstUserData()

Scope di una variabile

Introducendo le variabili, ho parlato di const, let e var.

Lo scope è il set di variabili che è visibile a una parte del programma.

In JavaScript ci sono scope globali, scope di blocco e scope di funzione.

Se una variabile è definita fuori da una funzione o da un blocco, è legata all'oggetto globale e ha uno scope globale, il che vuol dire che è disponibile in ogni parte del programma.

C'è una differenza molto importante tra le dichiarazioni var, let econst.

Una variabile definita con var dentro una funzione è visibile soltanto all'interno di quella funzione, come per gli argomenti di una funzione.

Una variabile definita con const o let, invece è visibile all'interno del blocco in cui è definita.

Un blocco è un insieme di istruzioni raggruppate all'interno di parentesi graffe, come quelle che troviamo dentro un'istruzione if, un loop for o una funzione.

È molto importante capire che un blocco non definisce un nuovo scope per var, ma lo fa per let econst.

Questo ha delle implicazioni pratiche rilevanti.

Supponiamo di definire una variabile con var all'interno di un condizionale if in una funzione:

function getData() {
  if (true) {
    var data = 'some data'
    console.log(data) 
  }
}

Richiamando questa funzione otterrai some data sulla console.

Se provi a spostare console.log(data) dopo l'istruzione if, funzionerà ancora:

function getData() {
  if (true) {
    var data = 'some data'
  }
  console.log(data) 
}

Ma se cambi var data in let data:

function getData() {
  if (true) {
    let data = 'some data'
  }
  console.log(data) 
}

Otterrai un messaggio di errore: ReferenceError: data is not defined.

Questo si verifica perché var ha uno scope di funzione e accade una cosa particolare chiamata hoisting. In breve, la dichiarazione var viene spostata in cima alla funzione più vicina da JavaScript prima dell'esecuzione del codice. Ecco come appare la funzione all'interno di JavaScript, più o meno:

function getData() {
  var data
  if (true) {
    data = 'some data'
  }
  console.log(data) 
}

Ecco perché puoi usare console.log(data) all'inizio di una funzione, addirittura prima che sia dichiarata e otterrai il valore undefined per quella variabile:

function getData() {
  console.log(data) 
  if (true) {
    var data = 'some data'
  }
}

Ma se cambi la dichiarazione in let, vedrai l'errore ReferenceError: data is not defined, perché l'hoisting non avviene con le dichiarazioni let.

const segue le stesse regole di let: il suo è uno scope di blocco.

Può essere ingannevole all'inizio, ma una volta capita questa differenza, vedrai perché l'uso di var non è considerato una buona pratica in confronto a let - le funzioni dichiarate con let hanno meno parti mobili, e il loro scope è limitato al blocco, il che le rende ottime variabili di loop perché smettono di esistere dopo che il loop ha finito:

function doLoop() {
  for (var i = 0; i < 10; i++) {
    console.log(i)
  }
  console.log(i)
}

doLoop()

Una volta uscito dal loop, i sarà una variabile valida con il valore 10.

Passando a let, se provi a usare console.log(i) otterrai l'errore ReferenceError: i is not defined.

Conclusione

Grazie per  aver letto questo manuale.

Spero che ti motiverà a imparare molto altro su JavaScript.

Per altre informazioni su JavaScript, visita il mio blog flaviocopes.com.