Padroneggia BigInt di JavaScript per calcoli precisi su interi di grandi dimensioni. Esplora sintassi, casi d'uso in crittografia e finanza, e supera le trappole comuni come la serializzazione JSON.
BigInt in JavaScript: Guida Completa al Calcolo di Numeri di Grandi Dimensioni
Per molti anni, gli sviluppatori JavaScript hanno affrontato una limitazione silenziosa ma significativa: la capacità nativa del linguaggio di gestire i numeri. Sebbene perfettamente adatto per i calcoli di tutti i giorni, il tipo Number
di JavaScript vacillava di fronte a interi veramente enormi richiesti in campi come la crittografia, il calcolo scientifico e i moderni sistemi di dati. Questo ha portato a un mondo di soluzioni alternative, librerie di terze parti e sottili errori di precisione difficili da debuggare.
Quell'era è finita. L'introduzione di BigInt come tipo primitivo nativo di JavaScript ha rivoluzionato il nostro modo di lavorare con numeri di grandi dimensioni. Fornisce un modo robusto, ergonomico ed efficiente per eseguire calcoli aritmetici su interi a precisione arbitraria, direttamente all'interno del linguaggio.
Questa guida completa è per gli sviluppatori di tutto il mondo. Approfondiremo il "perché, cosa e come" di BigInt. Che tu stia costruendo un'applicazione finanziaria, interagendo con una blockchain o semplicemente cercando di capire perché il tuo grande ID univoco proveniente da un'API si comporta in modo strano, questo articolo ti fornirà le conoscenze per padroneggiare BigInt.
Il Problema: I Limiti del Tipo Number di JavaScript
Prima di poter apprezzare la soluzione, dobbiamo comprendere appieno il problema. Per la maggior parte della sua storia, JavaScript ha avuto un solo tipo di numero: il tipo Number
. Dietro le quinte, è rappresentato come un numero in virgola mobile a 64 bit a doppia precisione IEEE 754. Questo formato è eccellente per rappresentare un'ampia gamma di valori, inclusi i decimali, ma ha una limitazione critica quando si tratta di interi.
Incontra MAX_SAFE_INTEGER
A causa della sua rappresentazione in virgola mobile, c'è un limite alla dimensione di un intero che può essere rappresentato con precisione perfetta. Questo limite è esposto tramite una costante: Number.MAX_SAFE_INTEGER
.
Il suo valore è 253 - 1, che è 9.007.199.254.740.991. Chiamiamolo nove quadrilioni per brevità.
Qualsiasi intero nell'intervallo da -Number.MAX_SAFE_INTEGER
a +Number.MAX_SAFE_INTEGER
è considerato un "intero sicuro". Ciò significa che può essere rappresentato esattamente e confrontato correttamente. Ma cosa succede quando usciamo da questo intervallo?
Vediamolo in azione:
const maxSafe = Number.MAX_SAFE_INTEGER;
console.log(maxSafe); // 9007199254740991
// Aggiungiamo 1
console.log(maxSafe + 1); // 9007199254740992 - Sembra corretto
// Aggiungiamo un altro 1
console.log(maxSafe + 2); // 9007199254740992 - Uh oh. Risultato errato.
// Va anche peggio
console.log(maxSafe + 3); // 9007199254740994 - Aspetta, cosa?
console.log(maxSafe + 4); // 9007199254740996 - Sta saltando dei numeri!
// Anche il controllo di uguaglianza fallisce
console.log(maxSafe + 1 === maxSafe + 2); // true - Questo è matematicamente sbagliato!
Come puoi vedere, una volta superato Number.MAX_SAFE_INTEGER
, JavaScript non può più garantire la precisione dei nostri calcoli. La rappresentazione numerica inizia ad avere delle lacune, portando a errori di arrotondamento e risultati errati. Questo è un incubo per le applicazioni che richiedono accuratezza con interi di grandi dimensioni.
Le Vecchie Soluzioni Alternative
Per anni, la comunità globale degli sviluppatori si è affidata a librerie esterne per risolvere questo problema. Librerie come bignumber.js
, decimal.js
e long.js
sono diventate strumenti standard. Funzionavano rappresentando i numeri grandi come stringhe o array di cifre e implementando le operazioni aritmetiche via software.
Sebbene efficaci, queste librerie comportavano dei compromessi:
- Overhead di Prestazioni: Le operazioni erano significativamente più lente rispetto ai calcoli con numeri nativi.
- Dimensione del Bundle: Aggiungevano peso ai bundle delle applicazioni, una preoccupazione per le prestazioni web.
- Sintassi Diversa: Gli sviluppatori dovevano usare metodi di oggetti (es.
a.add(b)
) invece degli operatori aritmetici standard (a + b
), rendendo il codice meno intuitivo.
Introduzione a BigInt: La Soluzione Nativa
BigInt è stato introdotto in ES2020 per risolvere questo problema in modo nativo. Un BigInt
è un nuovo tipo primitivo in JavaScript che fornisce un modo per rappresentare numeri interi più grandi di 253 - 1.
La caratteristica chiave di BigInt è che la sua dimensione non è fissa. Può rappresentare interi arbitrariamente grandi, limitati solo dalla memoria disponibile nel sistema host. Questo elimina completamente i problemi di precisione che abbiamo visto con il tipo Number
.
Come Creare un BigInt
Ci sono due modi principali per creare un BigInt:
- Aggiungendo `n` a un letterale intero: Questo è il metodo più semplice e comune.
- Usando la funzione costruttore `BigInt()`: Questo è utile quando si converte un valore da un altro tipo, come una stringa o un numero.
Ecco come appaiono nel codice:
// 1. Usando il suffisso 'n'
const myFirstBigInt = 900719925474099199n;
const anotherBigInt = 123456789012345678901234567890n;
// 2. Usando il costruttore BigInt()
const fromString = BigInt("98765432109876543210");
const fromNumber = BigInt(100);
// Puoi controllare il tipo
console.log(typeof myFirstBigInt); // "bigint"
console.log(typeof 100); // "number"
Con BigInt, il nostro calcolo precedente che falliva ora funziona perfettamente:
const maxSafePlusOne = BigInt(Number.MAX_SAFE_INTEGER) + 1n;
const maxSafePlusTwo = BigInt(Number.MAX_SAFE_INTEGER) + 2n;
console.log(maxSafePlusOne.toString()); // "9007199254740992"
console.log(maxSafePlusTwo.toString()); // "9007199254740993"
// L'uguaglianza funziona come previsto
console.log(maxSafePlusOne === maxSafePlusTwo); // false
Lavorare con BigInt: Sintassi e Operazioni
I BigInt si comportano in modo molto simile ai numeri normali, ma con alcune differenze cruciali che ogni sviluppatore deve capire per evitare bug.
Operazioni Aritmetiche
Tutti gli operatori aritmetici standard funzionano con i BigInt:
- Addizione:
+
- Sottrazione:
-
- Moltiplicazione:
*
- Elevamento a potenza:
**
- Modulo (Resto):
%
L'unico operatore che si comporta diversamente è la divisione (`/`).
const a = 10n;
const b = 3n;
console.log(a + b); // 13n
console.log(a - b); // 7n
console.log(a * b); // 30n
console.log(a ** b); // 1000n
console.log(a % b); // 1n
L'Avvertenza della Divisione
Poiché i BigInt possono rappresentare solo numeri interi, il risultato di una divisione viene sempre troncato (la parte frazionaria viene scartata). Non viene arrotondato.
const a = 10n;
const b = 3n;
console.log(a / b); // 3n (non 3.333...n)
const c = 9n;
const d = 10n;
console.log(c / d); // 0n
Questa è una distinzione critica. Se hai bisogno di eseguire calcoli con decimali, BigInt non è lo strumento giusto. Dovresti continuare a usare Number
o una libreria dedicata ai decimali.
Confronto e Uguaglianza
Gli operatori di confronto come >
, <
, >=
e <=
funzionano senza problemi tra BigInt, e anche tra un BigInt e un Number.
console.log(10n > 5); // true
console.log(10n < 20); // true
console.log(10n > 20n); // false
Tuttavia, l'uguaglianza è più sfumata ed è una fonte comune di confusione.
- Uguaglianza non rigorosa (`==`): Questo operatore esegue una coercizione di tipo. Considera uguali un BigInt e un Number con lo stesso valore matematico.
- Uguaglianza rigorosa (`===`): Questo operatore non esegue una coercizione di tipo. Poiché BigInt e Number sono tipi diversi, restituirà sempre
false
quando li confronta.
console.log(10n == 10); // true - Fai attenzione con questo!
console.log(10n === 10); // false - Consigliato per chiarezza.
console.log(0n == 0); // true
console.log(0n === 0); // false
Buona pratica: Per evitare bug sottili, usa sempre l'uguaglianza rigorosa (`===`) e sii esplicito sui tipi che stai confrontando. Se devi confrontare un BigInt e un Number, è spesso più chiaro convertire prima l'uno nell'altro, tenendo presente la potenziale perdita di precisione.
La Mancata Corrispondenza di Tipo: Una Separazione Rigorosa
JavaScript impone una regola rigida: non puoi mischiare operandi BigInt e Number nella maggior parte delle operazioni aritmetiche.
Tentarci comporterà un TypeError
. Questa è una scelta di progettazione deliberata per evitare che gli sviluppatori perdano accidentalmente precisione.
const myBigInt = 100n;
const myNumber = 50;
try {
const result = myBigInt + myNumber; // Questo lancerà un errore
} catch (error) {
console.log(error); // TypeError: Cannot mix BigInt and other types, use explicit conversions
}
L'Approccio Corretto: Conversione Esplicita
Per eseguire un'operazione tra un BigInt e un Number, devi convertire esplicitamente uno di essi.
const myBigInt = 100n;
const myNumber = 50;
// Converti Number in BigInt (sicuro)
const result1 = myBigInt + BigInt(myNumber);
console.log(result1); // 150n
// Converti BigInt in Number (potenzialmente non sicuro!)
const veryLargeBigInt = 900719925474099199n;
// Questo perderà precisione!
const unsafeNumber = Number(veryLargeBigInt);
console.log(unsafeNumber); // 900719925474099200 - Il valore è stato arrotondato!
const safeResult = Number(100n) + myNumber;
console.log(safeResult); // 150
Regola critica: Converti un BigInt in un Number solo se sei assolutamente certo che rientri nell'intervallo degli interi sicuri. Altrimenti, converti sempre il Number in un BigInt per mantenere la precisione.
Casi d'Uso Pratici per BigInt in un Contesto Globale
La necessità di BigInt non è un problema accademico astratto. Risolve sfide del mondo reale affrontate dagli sviluppatori in vari domini internazionali.
1. Timestamp ad Alta Precisione
Date.now()
di JavaScript restituisce il numero di millisecondi dall'epoca Unix. Sebbene sufficiente per molte applicazioni web, non è abbastanza granulare per sistemi ad alte prestazioni. Molti sistemi distribuiti, database e framework di logging in tutto il mondo utilizzano timestamp con precisione al nanosecondo per ordinare accuratamente gli eventi. Questi timestamp sono spesso rappresentati come interi a 64 bit, che sono troppo grandi per il tipo Number
.
// Un timestamp da un sistema ad alta risoluzione (es. in nanosecondi)
const nanoTimestampStr = "1670000000123456789";
// Usare Number causa una perdita di precisione
const lostPrecision = Number(nanoTimestampStr);
console.log(lostPrecision); // 1670000000123456800 - Errato!
// Usare BigInt lo preserva perfettamente
const correctTimestamp = BigInt(nanoTimestampStr);
console.log(correctTimestamp.toString()); // "1670000000123456789"
// Ora possiamo eseguire calcoli accurati
const oneSecondInNanos = 1_000_000_000n;
const nextSecond = correctTimestamp + oneSecondInNanos;
console.log(nextSecond.toString()); // "1670001000123456789"
2. Identificatori Univoci (ID) da API
Uno scenario molto comune è l'interazione con API che utilizzano interi a 64 bit per ID di oggetti univoci. Questo è un pattern utilizzato da importanti piattaforme globali come Twitter (ID Snowflake) e molti sistemi di database (es. tipo BIGINT
in SQL).
Quando recuperi dati da una tale API, il parser JSON nel tuo browser o ambiente Node.js potrebbe tentare di analizzare questo grande ID come un Number
, portando alla corruzione dei dati prima ancora che tu abbia la possibilità di lavorarci.
// Una tipica risposta JSON da un'API
// Nota: l'ID è un numero grande, non una stringa.
const jsonResponse = '{"id": 1367874743838343168, "text": "Hello, world!"}';
// JSON.parse standard corromperà l'ID
const parsedData = JSON.parse(jsonResponse);
console.log(parsedData.id); // 1367874743838343200 - ID sbagliato!
// Soluzione: Assicurarsi che l'API invii gli ID grandi come stringhe.
const safeJsonResponse = '{"id": "1367874743838343168", "text": "Hello, world!"}';
const safeParsedData = JSON.parse(safeJsonResponse);
const userId = BigInt(safeParsedData.id);
console.log(userId); // 1367874743838343168n - Corretto!
Questo è il motivo per cui è una buona pratica ampiamente accettata a livello mondiale che le API serializzino i grandi ID interi come stringhe nei payload JSON per garantire la compatibilità con tutti i client.
3. Crittografia
La crittografia moderna si basa su matematica che coinvolge interi estremamente grandi. Algoritmi come RSA si basano su operazioni con numeri lunghi centinaia o addirittura migliaia di bit. BigInt rende possibile eseguire questi calcoli nativamente in JavaScript, il che è essenziale per le applicazioni crittografiche basate sul web, come quelle che utilizzano la Web Crypto API o implementano protocolli in Node.js.
Sebbene un esempio crittografico completo sia complesso, possiamo vedere una dimostrazione concettuale:
// Due numeri primi molto grandi (solo a scopo dimostrativo)
const p = 1143400375533529n;
const q = 982451653n; // Uno più piccolo per l'esempio
// In RSA, li moltiplichi per ottenere il modulo
const n = p * q;
console.log(n.toString()); // "1123281328905333100311297"
// Questo calcolo sarebbe impossibile con il tipo Number.
// BigInt lo gestisce senza sforzo.
4. Applicazioni Finanziarie e Blockchain
Quando si ha a che fare con la finanza, specialmente nel contesto delle criptovalute, la precisione è fondamentale. Molte criptovalute, come Bitcoin, misurano il valore nella loro unità più piccola (es. satoshi). L'offerta totale di queste unità può facilmente superare Number.MAX_SAFE_INTEGER
. BigInt è lo strumento perfetto per gestire queste grandi e precise quantità senza ricorrere all'aritmetica in virgola mobile, che è soggetta a errori di arrotondamento.
// 1 Bitcoin = 100.000.000 satoshi
const satoshisPerBTC = 100_000_000n;
// L'offerta totale di Bitcoin è di 21 milioni
const totalBTCSupply = 21_000_000n;
// Calcola i satoshi totali
const totalSatoshis = totalBTCSupply * satoshisPerBTC;
// 2.100.000.000.000.000 - Questo è 2,1 quadrilioni
console.log(totalSatoshis.toString());
// Questo valore è più grande di Number.MAX_SAFE_INTEGER
console.log(totalSatoshis > BigInt(Number.MAX_SAFE_INTEGER)); // true
Argomenti Avanzati e Trappole Comuni
Serializzazione e JSON.stringify()
Uno dei problemi più comuni che gli sviluppatori affrontano è la serializzazione di oggetti contenenti BigInt. Di default, JSON.stringify()
non sa come gestire il tipo bigint
e lancerà un TypeError
.
const data = {
id: 12345678901234567890n,
user: 'alex'
};
try {
JSON.stringify(data);
} catch (error) {
console.log(error); // TypeError: Do not know how to serialize a BigInt
}
Soluzione 1: Implementare un metodo `toJSON`
Puoi dire a `JSON.stringify` come gestire i BigInt aggiungendo un metodo `toJSON` a `BigInt.prototype`. Questo approccio modifica il prototipo globale, il che potrebbe non essere desiderabile in alcuni ambienti condivisi, ma è molto efficace.
// Una patch globale. Usare con cautela.
BigInt.prototype.toJSON = function() {
return this.toString();
};
const data = { id: 12345678901234567890n, user: 'alex' };
const jsonString = JSON.stringify(data);
console.log(jsonString); // '{"id":"12345678901234567890","user":"alex"}'
Soluzione 2: Usare una funzione `replacer`
Un approccio più sicuro e localizzato consiste nell'utilizzare l'argomento `replacer` in `JSON.stringify`. Questa funzione viene chiamata per ogni coppia chiave/valore e consente di trasformare il valore prima della serializzazione.
const data = { id: 12345678901234567890n, user: 'alex' };
const replacer = (key, value) => {
if (typeof value === 'bigint') {
return value.toString();
}
return value;
};
const jsonString = JSON.stringify(data, replacer);
console.log(jsonString); // '{"id":"12345678901234567890","user":"alex"}'
Operazioni Bitwise
BigInt supporta tutti gli operatori bitwise che conosci dal tipo `Number`: `&` (AND), `|` (OR), `^` (XOR), `~` (NOT), `<<` (shift a sinistra) e `>>` (shift a destra con propagazione del segno). Questi sono particolarmente utili quando si lavora con formati di dati a basso livello, permessi o certi tipi di algoritmi.
const permissions = 5n; // 0101 in binario
const READ_PERMISSION = 4n; // 0100
const WRITE_PERMISSION = 2n; // 0010
// Controlla se il permesso di lettura è impostato
console.log((permissions & READ_PERMISSION) > 0n); // true
// Controlla se il permesso di scrittura è impostato
console.log((permissions & WRITE_PERMISSION) > 0n); // false
// Aggiungi il permesso di scrittura
const newPermissions = permissions | WRITE_PERMISSION;
console.log(newPermissions); // 7n (che è 0111)
Considerazioni sulle Prestazioni
Sebbene BigInt sia incredibilmente potente, è importante comprendere le sue caratteristiche prestazionali:
- Number vs. BigInt: Per gli interi all'interno dell'intervallo sicuro, le operazioni standard con `Number` sono significativamente più veloci. Questo perché possono spesso mappare direttamente le istruzioni a livello macchina elaborate dalla CPU del computer. Le operazioni con BigInt, essendo di dimensioni arbitrarie, richiedono algoritmi più complessi basati su software.
- BigInt vs. Librerie: Il `BigInt` nativo è generalmente molto più veloce delle librerie per numeri grandi basate su JavaScript. L'implementazione fa parte del motore JavaScript (come V8 o SpiderMonkey) ed è scritta in un linguaggio di livello inferiore come C++, il che gli conferisce un notevole vantaggio in termini di prestazioni.
La Regola d'Oro: Usa `Number` per tutti i calcoli numerici a meno che tu non abbia un motivo specifico per credere che i valori possano superare `Number.MAX_SAFE_INTEGER`. Usa `BigInt` quando hai bisogno delle sue capacità, non come sostituto predefinito per tutti i numeri.
Compatibilità con Browser e Ambienti
BigInt è una funzionalità moderna di JavaScript, ma il suo supporto è ora diffuso in tutto l'ecosistema globale.
- Browser Web: Supportato in tutti i principali browser moderni (Chrome 67+, Firefox 68+, Safari 14+, Edge 79+).
- Node.js: Supportato dalla versione 10.4.0.
Per i progetti che devono supportare ambienti molto vecchi, la traspilazione tramite strumenti come Babel può essere un'opzione, ma ciò comporta una penalizzazione delle prestazioni. Dato l'ampio supporto odierno, la maggior parte dei nuovi progetti può utilizzare BigInt in modo nativo senza preoccupazioni.
Conclusione e Buone Pratiche
BigInt è un'aggiunta potente ed essenziale al linguaggio JavaScript. Fornisce una soluzione nativa, efficiente ed ergonomica al problema di lunga data dell'aritmetica con interi di grandi dimensioni, consentendo la creazione di una nuova classe di applicazioni con JavaScript, dalla crittografia alla gestione di dati ad alta precisione.
Per usarlo efficacemente ed evitare le trappole comuni, tieni a mente queste buone pratiche:
- Usa il Suffisso `n`: Preferisci la sintassi letterale `123n` per creare BigInt. È chiara, concisa ed evita potenziali perdite di precisione durante la creazione.
- Non Mischiare i Tipi: Ricorda che non puoi mischiare BigInt e Number nelle operazioni aritmetiche. Sii esplicito con le conversioni: `BigInt()` o `Number()`.
- Dai Priorità alla Precisione: Quando converti tra tipi, favorisci sempre la conversione di un `Number` in un `BigInt` per prevenire perdite di precisione accidentali.
- Usa l'Uguaglianza Rigorosa: Usa `===` invece di `==` per i confronti per evitare comportamenti confusi causati dalla coercizione di tipo.
- Gestisci la Serializzazione JSON: Pianifica la serializzazione dei BigInt. Usa una funzione `replacer` personalizzata in `JSON.stringify` per una soluzione sicura e non globale.
- Scegli lo Strumento Giusto: Usa `Number` per la matematica generica all'interno dell'intervallo degli interi sicuri per prestazioni migliori. Ricorri a `BigInt` solo quando hai veramente bisogno delle sue capacità di precisione arbitraria.
Abbracciando BigInt e comprendendone le regole, puoi scrivere applicazioni JavaScript più robuste, accurate e potenti, in grado di affrontare sfide numeriche di qualsiasi scala.