Un solo tipo per (quasi) tutti i numeri
La maggior parte dei linguaggi ti dà tipi separati per interi e decimali. JavaScript, storicamente, te ne dava uno solo: Number. Che scrivessi 42, 3.14 o -0.001, ottenevi sempre lo stesso primitivo, un numero in virgola mobile a doppia precisione a 64 bit secondo lo standard IEEE 754.
È comodo: niente conversioni tra int e float, niente overflow a 2^31. Ma la rappresentazione in virgola mobile ha delle conseguenze, e colgono spesso di sorpresa chi inizia. Nel 2020 è stato aggiunto un secondo tipo numerico, BigInt, proprio per i casi in cui Number non basta.
La sorpresa della virgola mobile
Esegui questo:
La prima riga stampa 0.30000000000000004. La seconda stampa false. Non è una stranezza di JavaScript: Python, Java, C e qualsiasi linguaggio che usa numeri in virgola mobile IEEE 754 si comporta allo stesso modo.
Il motivo: 0.1 e 0.2 non si possono rappresentare in modo esatto in binario, proprio come 1/3 non si può scrivere in modo esatto in decimale. Viene salvata l'approssimazione binaria più vicina, e i piccoli errori si accumulano. Il modello mentale giusto è questo: tratta i valori Number con decimali come approssimazioni che per caso sono molto vicine a quello che hai scritto.
Per il denaro, non salvare $19.99 come 19.99. Salva i centesimi come interi, 1999, e formattali quando li mostri. È l'abitudine migliore in assoluto per evitare i bug della virgola mobile.
Confrontare numeri decimali in sicurezza
Visto che l'uguaglianza non è affidabile, quando serve confronta con una tolleranza:
Number.EPSILON è la differenza più piccola tra 1 e il numero rappresentabile successivo: una tolleranza predefinita ragionevole per valori vicini a 1. Per grandezze molto grandi o molto piccole ti servirà una tolleranza che si adatti alla scala dei valori.
L'intervallo degli interi sicuri in JavaScript
Gli interi fino a una certa dimensione sono esatti in un numero in virgola mobile a 64 bit. Oltre quella soglia inizi a perdere precisione un bit alla volta:
2^53 - 1 è l'ultimo intero per cui tutti gli interi più piccoli sono rappresentabili. Oltre quel punto, alcuni interi semplicemente non esistono nel tipo Number: vengono arrotondati al vicino più prossimo. È un bug silenzioso di corruzione dei dati pronto a scattare se leggi ID a 64 bit da un database come normali numeri JSON.
Arriva BigInt
BigInt è un primitivo a parte per interi a precisione arbitraria. Ne crei uno aggiungendo n a un letterale intero, oppure chiamando BigInt(...):
I BigInt non hanno altro limite superiore che la memoria disponibile. Sono lo strumento giusto per:
- ID di database o snowflake ID di Twitter/X che superano
2^53. - Calcoli crittografici.
- Qualsiasi aritmetica intera in cui risultati esatti contano più della velocità pura.
Non sono invece lo strumento giusto per i contatori di tutti i giorni, gli indici degli array o il denaro in centesimi: il normale Number è più veloce e funziona con tutte le API del linguaggio.
Aritmetica con BigInt
Tutti gli operatori abituali funzionano, a patto che entrambi gli operandi siano BigInt:
La divisione tronca verso lo zero: non esistono BigInt frazionari. Se ti serve una frazione, torni nel territorio di Number (o di una libreria decimale).
Non mescolare i tipi
La regola su cui tutti inciampano: non puoi mescolare Number e BigInt nella stessa espressione.
Il confronto è l'unica eccezione: <, > e == fanno la coercizione tra i due tipi:
Quindi == li considera uguali, === no. Se usi già === ovunque (e dovresti), considera i confronti numerici tra i due tipi un campanello d'allarme nel design: scegli un lato e converti.
Convertire tra number e bigint
Due conversioni, due trappole:
Passare da Number a BigInt è rigido: decimali e NaN lanciano un errore. Passare da BigInt a Number è permissivo ma perde precisione: tutto ciò che supera MAX_SAFE_INTEGER viene arrotondato. Se stai convertendo un BigInt ricevuto da un server, chiediti prima se ti serve davvero.
Valori speciali del tipo Number
Già che ci siamo, ci sono tre valori del tipo Number che non sono numeri in senso matematico:
Infinity e -Infinity compaiono quando dividi per zero o superi l'intervallo dei numeri in virgola mobile. NaN ("not a number") compare quando un'operazione aritmetica non produce un risultato sensato.
NaN è famoso per non essere uguale a sé stesso: fa parte dello standard IEEE 754, non è un bug di JS. Per controllarlo usa Number.isNaN(x). Il vecchio isNaN globale prima converte l'argomento, e questo dà risposte sbagliate (isNaN("hello") restituisce true). Preferisci sempre la versione Number.isNaN.
Convertire stringhe in numeri in JavaScript
L'input dell'utente e i numeri JSON arrivano spesso come stringhe. Ci sono tre modi per convertirli:
Number() è rigido: qualsiasi cosa non numerica dà NaN, tranne la stringa vuota e gli spazi, che danno 0. parseInt e parseFloat sono tolleranti: leggono finché possono e poi si fermano. Scegli quello che corrisponde alla tua intenzione e controlla se il risultato è NaN prima di usarlo.
Per leggere BigInt da stringhe, usa BigInt("123"): è rigido e lancia un'eccezione se l'input non è valido.
Regole rapide
- Per contatori, calcoli, coordinate e la maggior parte dei numeri di tutti i giorni: usa
Number. - Per il denaro: converti in centesimi interi e usa
Number, oppure ricorri a una libreria decimale. - Per interi più grandi di
2^53(ID di database, crittografia, calcolo combinatorio): usaBigIntcon il suffisson. - Confronta i numeri in virgola mobile con una tolleranza, non con
===. - Rileva i risultati non validi con
Number.isNaNeNumber.isFinite, non con le versioni globali. - Non mescolare
NumbereBigIntnella stessa espressione: converti in modo esplicito.
Prossimo passo: null vs undefined
JavaScript ha due modi per dire "nessun valore", null e undefined, e non sono intercambiabili. Nella prossima pagina vediamo cosa significa ciascuno, in cosa differiscono e quando usare l'uno o l'altro.
Domande frequenti
Perché 0.1 + 0.2 non fa 0.3 in JavaScript?
Perché il tipo Number di JavaScript è un numero in virgola mobile a 64 bit secondo lo standard IEEE 754, e né 0.1 né 0.2 si possono rappresentare in modo esatto in binario. Il risultato è 0.30000000000000004. Non è un bug di JavaScript: succede lo stesso in Python, Java e in qualsiasi altro linguaggio che usa lo stesso formato. Per il denaro, lavora con interi (centesimi) o usa una libreria decimale.
Cos'è BigInt in JavaScript e quando conviene usarlo?
BigInt è un primitivo numerico a parte, pensato per gli interi oltre Number.MAX_SAFE_INTEGER (2^53 - 1). Lo crei aggiungendo una n finale, come in 9007199254740993n, oppure con BigInt(valore). Usalo per ID di database a 64 bit, crittografia o qualsiasi calcolo intero in cui la precisione conta più della velocità.
Posso mescolare Number e BigInt in JavaScript?
No. 1n + 1 lancia TypeError: Cannot mix BigInt and other types. Devi convertire in modo esplicito con BigInt(n) o Number(b). Gli operatori di confronto come < e == funzionano tra i due tipi, ma === restituisce false perché i tipi sono diversi.