Vorremo rendere disponibile questo progetto open-source per persone in tutto il mondo.

Aiutaci a tradurre il contenuto di questo tutorial nella tua lingua!

Nella versione moderna di JavaScript ci sono due differenti tipi di numeri:

  1. I numeri regolari, che vengono memorizzati nel formato a 64 bit IEEE-754, conosciuti anche come “numeri in virgola mobile con doppia precisione”. Questi sono i numeri che utilizziamo la maggior parte del tempo, e sono quelli di cui parleremo in questo capitolo.

  2. I BigInt, che vengono utilizzati per rappresentare numeri interi di lunghezza arbitraria. Talvolta possono tornare utili, poiché i numeri regolari non possono eccedere 253 od essere inferiori di -253. Poiché i BigInt vengono utilizzati in alcune aree speciali, gli abbiamo dedicato un capitolo BigInt.

Quindi in questo capitolo parleremo dei numeri regolari.

Diversi modi di scrivere un numero

Immaginiamo di dover scrivere 1 milione. La via più ovvia è:

let billion = 1000000000;

Possiamo anche usare il carattere underscore _ come separatore:

let billion = 1_000_000_000;

Qui il carattere _ gioca il ruolo di “zucchero sintattico”, cioè rende il numero più leggibile. Il motore JavaScript semplicemente ignorerà i caratteri _ tra le cifre, quindi è equivalente al milione scritto sopra.

Nella vita reale però cerchiamo di evitare di scrivere lunghe file di zeri per evitare errori. E anche perché siamo pigri. Solitamente scriviamo qualcosa del tipo "1ml" per un milione o "7.3ml" 7 milioni e 300mila. Lo stesso vale per i numeri più grandi.

In JavaScript, possiamo abbreviare un numero inserendo la lettera "e" con il numero di zeri a seguire:

let billion = 1e9;  // 1 miliardo, letteralmente: 1 e 9 zeri

alert( 7.3e9 );  // 7.3 miliardi (equivale a 7300000000 o 7_300_000_000)

In altre parole, "e" moltiplica il numero 1 seguito dal numero di zeri dati.

1e3 = 1 * 1000 // e3 significa *1000
1.23e6 = 1.23 * 1000000 // e6 significa *1000000

Ora proviamo a scrivere qualcosa di molto piccolo. Ad esempio, 1 microsecondo (un milionesimo di secondo):

let ms = 0.000001;

Come prima, l’utilizzo di "e" può aiutare. Se volessimo evitare di scrivere esplicitamente tutti gli “0”, potremmo scrivere:

let ms = 1e-6; // sei zeri alla sinistra di 1

Se contiamo gli zeri in 0.000001, ce ne sono 6. Quindi ovviamente 1e-6.

In altre parole, un numero negativo dopo "e" significa una divisione per 1 seguito dal numero di zeri dati:

// -3 divide 1 con 3 zeri
1e-3 = 1 / 1000; // 0.001

// -6 divide 1 con 6 zeri
1.23e-6 = 1.23 / 1000000; // 0.00000123

Numeri esadecimali, binari e ottali

I numeri esadecimali (hexadecimal o hex) sono largamente utilizzati in JavaScript per rappresentare colori, codifiche di caratteri, e molte altre cose. Quindi ovviamente esiste un modo per abbreviarli: 0x e poi il numero.

Ad esempio:

alert( 0xff ); // 255
alert( 0xFF ); // 255 (equivalente)

I sistemi binario e ottale sono utilizzati raramente, ma sono comunque supportati con l’utilizzo dei prefissi 0b e 0o:

let a = 0b11111111; // forma binaria di 255
let b = 0o377; // forma ottale di 255

alert( a == b ); // true, lo stesso numero 255 da entrambe le parti

Ci sono solo 3 sistemi di numerazione con questo livello di supporto. Per gli altri sistemi, dovremmo utilizzare la funzione parseInt (che vedremo più avanti in questo capitolo).

toString(base)

Il metodo num.toString(base) ritorna una rappresentazione in stringa del numero num con il sistema di numerazione fornito base.

Ad esempio:

let num = 255;

alert( num.toString(16) );  // ff
alert( num.toString(2) );   // 11111111

La base può variare da 2 a 36. Di default vale 10.

Altri casi di uso comune sono:

  • base=16 si utilizza per colori in esadecimale, codifiche di caratteri, i caratteri supportati sono 0..9 o A..F.

  • base=2 viene utilizzato per debugging o operazioni bit a bit, i caratteri accettati sono 0 o 1.

  • base=36 la base massima, i caratteri possono andare da 0..9 o A..Z. L’intero alfabeto latino viene utilizzato per rappresentare un numero. Un caso divertente di utilizzo per la base 36 è quando abbiamo bisogno che un identificatore molto lungo diventi qualcosa di più breve, come ad esempio per accorciare gli url. Possiamo semplicemente rappresentarlo in base 36:

    alert( 123456..toString(36) ); // 2n9c
Due punti per chiamare un metodo

Da notare che i due punti in 123456..toString(36) non sono un errore. Se vogliamo chiamare un metodo direttamente da un numero, come toString nell’esempio sopra, abbiamo bisogno di inserire due punti ...

Se inseriamo un solo punto: 123456.toString(36), otterremo un errore, perché la sintassi JavaScript implica una parte decimale a seguire del primo punto. Se invece inseriamo un ulteriore punto, allora JavaScript capirà che la parte decimale è vuota e procederà nel chiamare il metodo.

Potremmo anche scrivere (123456).toString(36).

Arrotondare

Una delle operazioni più utilizzate quando lavoriamo con i numeri è l’arrotondamento.

Ci sono diverse funzioni integrate per eseguire questa operazione:

Math.floor
Arrotonda per difetto: 3.1 diventa 3, e -1.1 diventa -2.
Math.ceil
Arrotonda per eccesso: 3.1 diventa 4, e -1.1 diventa -1.
Math.round
Arrotonda all’intero più vicino: 3.1 diventa 3, 3.6 diventa 4, e 3.5 viene arrotondato anch’esso a 4.
Math.trunc (non supportato da Internet Explorer)
Rimuove tutto dopo la virgola decimale senza arrotondare: 3.1 diventa 3, -1.1 diventa -1.

Qui abbiamo una tabella che riassume le principali differenze:

Math.floor Math.ceil Math.round Math.trunc
3.1 3 4 3 3
3.6 3 4 4 3
-1.1 -2 -1 -1 -1
-1.6 -2 -1 -2 -1

Queste funzioni coprono tutti i possibili casi quando trattiamo numeri con una parte decimale. Come potremmo fare se volessimo arrotondare il numero ad n cifre dopo la virgola?

Ad esempio, abbiamo 1.2345 e vogliamo arrotondarlo a due cifre dopo la virgola, tenendo solo 1.23.

Ci sono due modi per farlo:

  1. Moltiplica e dividi.

    Ad esempio, per arrotondare un numero alla seconda cifra decimale, possiamo moltiplicare il numero per 100, chiamare la funzione di arrotondamento e dividerlo nuovamente.

    let num = 1.23456;
    
    alert( Math.round(num * 100) / 100 ); // 1.23456 -> 123.456 -> 123 -> 1.23
  2. Il metodo toFixed(n) arrotonda il numero a n cifre dopo la virgola e ritorna una rappresentazione in stringa del risultato.

    let num = 12.34;
    alert( num.toFixed(1) ); // "12.3"

    Questo metodo arrotonderà per difetto o per eccesso in base al valore più vicino, similmente a Math.round:

    let num = 12.36;
    alert( num.toFixed(1) ); // "12.4"

    Da notare che il risultato di toFixed è una stringa. Se la parte decimale è più breve di quanto richiesto, verranno aggiunti degli zeri:

    let num = 12.34;
    alert( num.toFixed(5) ); // "12.34000", aggiunti gli zeri per renderlo esattamente di 5 cifre decimali

    Possiamo convertire il risultato al tipo numerico utilizzando la somma unaria o chiamando il metodo Number(): +num.toFixed(5).

Calcoli imprecisi

Internamente, un numero è rappresentato in formato 64-bit IEEE-754, quindi vengono utilizzati esattamente 64 bit per rappresentare un numero: 52 vengono utilizzati per rappresentare le cifre, 11 per la parte decimale, e infine 1 bit per il segno.

Se un numero è troppo grande, tale da superare i 64 bit disponibili, come ad esempio un numero potenzialmente infinito:

alert( 1e500 ); // Infinity

Potrebbe essere poco ovvio, ma quello che accade è la perdita di precisione.

Consideriamo questo test (falso!):

alert( 0.1 + 0.2 == 0.3 ); // false

Esatto, se provassimo a confrontare il risultato della somma tra 0.1 e 0.2 con 0.3, otterremmo false.

Strano! Quale può essere il risultato se non 0.3?

alert( 0.1 + 0.2 ); // 0.30000000000000004

Ouch! Un confronto errato di questo tipo può generare diverse conseguenze. Immaginate di progettare un sito di e-shop in cui i visitatori aggiungono al carrello articoli da $0.10 e $0.20. Poi come prezzo totale viene mostrato $0.30000000000000004. Questo risultato lascerebbe sorpreso chiunque.

Ma perché accade questo?

Un numero viene memorizzato nella sua forma binaria, una sequenza di “1” e “0”. I numeri con virgola come 0.1, 0.2 che visti nella loro forma decimale sembrano semplici, sono in realtà una sequenza infinita di cifre nella forma binaria.

In altre parole, cos’è 0.1? Vale 1 diviso 10 1/10, “un decimo”. Nel sistema decimale questi numeri sono facilmente rappresentabili. Prendiamo invece “un terzo”: 1/3. Diventa un numero con infiniti decimali 0.33333(3).

Quindi, le divisioni per potenze di 10 funzionano molto bene nel sistema decimale, non vale lo stesso con la divisione per 3. Per la stessa ragione, nel sistema binario le divisioni per potenze di 2 sono una garanzia, ma 1/10 diventa una sequenza infinita di cifre.

Non esiste alcun modo per rappresentare esattamente 0.1 o esattamente 0.2 usando il sistema binario, proprio come non è possibile memorizzare “un terzo” come decimale.

Il formato numerico IEEE-754 cerca di risolvere questo arrotondando al più vicino numero possibile. Questo tipo di arrotondamento non ci consente di vedere le “piccole perdite di precisione”, quindi il numero viene mostrato come 0.3. Ma comunque è presente una perdita.

Possiamo vedere un esempio:

alert( 0.1.toFixed(20) ); // 0.10000000000000000555

E quando sommiamo due numeri, la loro “perdita di precisione” viene incrementata.

Questo è il motivo per cui 0.1 + 0.2 non vale esattamente 0.3.

Non solo JavaScript

Lo stesso problema esiste in molti altri linguaggi di programmazione.

PHP, Java, C, Perl, Ruby hanno lo stesso tipo di problema, poiché si basano sullo stesso formato numerico.

Possiamo risolvere questo problema? Certamente, ci sono diverse soluzioni:

  1. Possiamo arrotondare il risultato con un metodo toFixed(n):
let sum = 0.1 + 0.2;
alert( sum.toFixed(2) ); // 0.30

Da notare che toFixed ritorna sempre una stringa. Viene cosi garantito che ci siano almeno due cifre dopo la virgola decimale. Questo ci torna molto utile se abbiamo un e-shopping e vogliamo mostrare $0.30. Per tutti gli altri casi possiamo semplicemente chiamare la conversione con l’operatore di somma unaria:

let sum = 0.1 + 0.2;
alert( +sum.toFixed(2) ); // 0.3
  1. Possiamo temporaneamente convertire i numeri ad interi per eseguire le operazioni e poi riconvertirli. In questo modo:
alert( (0.1 * 10 + 0.2 * 10) / 10 ); // 0.3
alert( (0.28 * 100 + 0.14 * 100) / 100); // 0.4200000000000001

Questo funziona perché quando facciamo 0.1 * 10 = 1 e 0.2 * 10 = 2 entrambi diventano interi, non vi è quindi perdita di precisione.

  1. Se abbiamo a che fare con dei prezzi, la miglior soluzione rimane quella di memorizzare tutti i prezzi in centesimi, evitando quindi di utilizzare i numeri con virgola. Ma cosa succede se proviamo ad applicare uno sconto del 30%? Nella pratica, evitare completamente questo problema è difficile, in alcuni casi possono tornare utili entrambe le soluzioni viste sopra.

Quindi, l’approccio moltiplicazione/divisione riduce gli errori, ma non li elimina completamente.

Talvolta possiamo evitare le frazioni. Ad esempio se abbiamo a che fare con un negozio, allora possiamo memorizzare i prezzi in centesimi piuttosto che in euro.

La cosa divertente

Provate ad eseguire questo:

// Ciao! Sono un numero autoincrementante!
alert( 9999999999999999 ); // mostra 10000000000000000

Questo esempio ha lo stesso problema: perdita di precisione. Per la rappresentazione di un numero sono disponibili 64bit, ne vengono utilizzati 52 per le cifre, questi potrebbero non essere sufficienti. Quindi le cifre meno significative vengono perse.

JavaScript non mostra errori in questi casi. Semplicemente fa del suo meglio per “farci stare” il numero, anche se il formato non è “grande” abbastanza.

Due zeri

Un’altra conseguenza divertente della rappresentazione interna è l’esistenza di due zeri: 0 e -0.

Questo perché il segno viene rappresentato con un solo bit, in questo modo ogni numero può essere positivo o negativo, lo stesso vale per lo zero.

Nella maggior parte dei casi questa differenza è impercettibile, poiché gli operatori sono studiati per trattarli allo stesso modo.

Test: isFinite e isNaN

Ricordate questi due valori numerici speciali?

  • Infinity (e -Infinity) è uno speciale valore che è più grande (o più piccolo) di qualsiasi altro numero.
  • NaN rappresenta un errore.

Questi appartengono al tipo number, ma non sono dei numeri “normali”, esistono quindi delle funzioni dedicate per la loro verifica:

  • isNaN(value) converte l’argomento al tipo numerico e successivamente verifica se è un NaN:

    alert( isNaN(NaN) ); // true
    alert( isNaN("str") ); // true

    Ma abbiamo veramente bisogno di questa funzione? Non possiamo semplicemente usare il confronto === NaN? Purtroppo la risposta è no. Il valore NaN è unico in questo aspetto, non è uguale a niente, nemmeno a se stesso:

    alert( NaN === NaN ); // false
  • isFinite(value) converte l’argomento al tipo numerico e ritorna true se questo è un numero diverso da NaN/Infinity/-Infinity:

    alert( isFinite("15") ); // true
    alert( isFinite("str") ); // false, perché rappresenta un valore speciale: NaN
    alert( isFinite(Infinity) ); // false, perché rappresenta un valore speciale: Infinity

In alcuni casi isFinite viene utilizzato per verificare se una stringa qualunque è un numero: