Menu

Liczby i BigInt w JavaScript: precyzja i duże liczby całkowite

Jak naprawdę działa typ Number w JavaScript: precyzja liczb zmiennoprzecinkowych, MAX_SAFE_INTEGER i kiedy lepiej sięgnąć po BigInt.

Na tej stronie są działające edytory: edytuj, uruchamiaj i od razu zobacz wynik.

Jeden typ liczbowy do wszystkiego (prawie)

Większość języków daje osobne typy dla liczb całkowitych i zmiennoprzecinkowych. JavaScript historycznie dawał jeden: Number. Niezależnie od tego, czy piszesz 42, 3.14 czy -0.001, dostajesz ten sam typ prosty: 64-bitową liczbę zmiennoprzecinkową podwójnej precyzji IEEE 754.

To wygodne: żadnego rzutowania między int i float, żadnego przepełnienia przy 2^31. Ale reprezentacja zmiennoprzecinkowa ma swoje konsekwencje i regularnie zaskakuje początkujących. Drugi typ liczbowy, BigInt, dodano w 2020 roku, żeby obsłużyć przypadki, w których Number nie wystarcza.

Niespodzianka z liczbami zmiennoprzecinkowymi

Uruchom to:

Pierwsza linia wypisuje 0.30000000000000004. Druga wypisuje false. To nie jest dziwactwo JavaScriptu: Python, Java, C i każdy język używający liczb zmiennoprzecinkowych IEEE 754 zachowuje się tak samo.

Powód: 0.1 i 0.2 nie da się dokładnie zapisać w systemie dwójkowym, tak jak 1/3 nie da się dokładnie zapisać w dziesiętnym. Zapisywane jest najbliższe przybliżenie binarne, a drobne błędy się kumulują. Model myślowy: traktuj wartości Number z częścią ułamkową jak przybliżenia, które akurat są bardzo bliskie temu, co napisano.

Pieniędzy nie przechowuj jako 19.99 dla kwoty $19.99. Przechowuj centy jako liczby całkowite, 1999, i formatuj przy wyświetlaniu. To najlepszy pojedynczy nawyk, który chroni przed błędami liczb zmiennoprzecinkowych.

Bezpieczne porównywanie liczb zmiennoprzecinkowych

Skoro równość jest zawodna, w razie potrzeby porównuj z tolerancją:

Number.EPSILON to najmniejsza różnica między 1 a następną liczbą, którą da się zapisać. To rozsądna domyślna tolerancja dla wartości bliskich 1. Dla bardzo dużych lub bardzo małych wartości potrzebna jest tolerancja, która skaluje się razem z danymi.

Zakres bezpiecznych liczb całkowitych

Liczby całkowite do pewnego rozmiaru są dokładne w 64-bitowej liczbie zmiennoprzecinkowej. Powyżej tej granicy precyzja zaczyna uciekać bit po bicie:

2^53 - 1 to ostatnia liczba całkowita, poniżej której da się zapisać każdą liczbę całkowitą. Dalej niektóre liczby całkowite po prostu nie istnieją w typie Number: zaokrąglają się do najbliższego sąsiada. To ciche uszkodzenie danych, które tylko czeka, żeby się wydarzyć, jeśli parsujesz 64-bitowe identyfikatory z bazy danych jako zwykłe liczby JSON.

Wchodzi BigInt

BigInt to osobny typ prosty dla liczb całkowitych o dowolnej precyzji. Tworzysz go, dopisując n do literału całkowitego, albo wywołując BigInt(...):

BigInty nie mają górnej granicy poza dostępną pamięcią. To właściwe narzędzie do:

  • Identyfikatorów z bazy danych albo identyfikatorów snowflake z Twittera/X, które przekraczają 2^53.
  • Obliczeń kryptograficznych.
  • Każdej arytmetyki na liczbach całkowitych, w której dokładny wynik liczy się bardziej niż surowa szybkość.

To nie jest dobre narzędzie do codziennych liczników, indeksów tablic czy pieniędzy w centach: zwykły Number jest szybszy i współpracuje z każdym API w języku.

Arytmetyka na BigInt

Działają wszystkie zwykłe operatory, o ile oba argumenty są BigIntami:

Dzielenie obcina wynik w stronę zera: nie ma ułamkowych BigIntów. Jeśli potrzebujesz ułamka, wracasz na teren Number (albo biblioteki do liczb dziesiętnych).

Nie mieszaj typów

Zasada, o którą ludzie się potykają: nie da się mieszać Number i BigInt w jednym wyrażeniu.

Jedynym wyjątkiem jest porównywanie: <, > i == konwertują wartości ponad tą granicą:

Czyli == uznaje je za równe, a === nie. Jeśli wszędzie używasz już === (i warto), traktuj porównania liczbowe między tymi dwoma typami jako sygnał problemu w projekcie: wybierz jedną stronę i skonwertuj.

Konwersja między typami

Dwie konwersje, dwie pułapki:

Konwersja Number → BigInt jest rygorystyczna: części ułamkowe i NaN rzucają błąd. Konwersja BigInt → Number jest pobłażliwa, ale stratna: wszystko powyżej MAX_SAFE_INTEGER się zaokrągla. Jeśli konwertujesz BigInt otrzymany z serwera, zastanów się, czy naprawdę musisz.

Specjalne wartości liczbowe

Skoro już tu jesteśmy: trzy wartości typu Number, które nie są liczbami w sensie matematycznym:

Infinity i -Infinity pojawiają się, gdy dzielisz przez zero albo przekraczasz zakres liczb zmiennoprzecinkowych. NaN ("not a number", czyli "nie liczba") pojawia się, gdy działanie arytmetyczne nie daje sensownego wyniku.

NaN słynie z tego, że nie jest równe samemu sobie: to część specyfikacji IEEE 754, a nie błąd JS. Do sprawdzania używaj Number.isNaN(x). Starsza globalna funkcja isNaN najpierw konwertuje argument, co daje błędne odpowiedzi (isNaN("hello") zwraca true). Zawsze wybieraj wersję Number.isNaN.

Parsowanie liczb ze stringów

Dane od użytkownika i liczby z JSON często przychodzą jako stringi. Trzy sposoby konwersji:

Number() jest rygorystyczny: wszystko, co nie jest liczbą, daje NaN, poza pustym stringiem i białymi znakami, które dają 0. parseInt i parseFloat są wyrozumiałe: czytają, ile się da, i się zatrzymują. Wybierz ten, który pasuje do twojego zamiaru, i sprawdź NaN, zanim użyjesz wyniku.

Do parsowania BigIntów ze stringów używaj BigInt("123"): jest rygorystyczny i rzuca błąd przy złych danych.

Krótki zbiór zasad

  • Do liczników, obliczeń, współrzędnych i większości codziennych liczb: używaj Number.
  • Do pieniędzy: przeskaluj do całkowitej liczby centów i używaj Number albo sięgnij po bibliotekę do liczb dziesiętnych.
  • Do liczb całkowitych większych niż 2^53 (identyfikatory z bazy danych, kryptografia, kombinatoryka): używaj BigInt z przyrostkiem n.
  • Porównuj liczby zmiennoprzecinkowe z tolerancją, a nie przez ===.
  • Błędne wyniki sprawdzaj przez Number.isNaN i Number.isFinite, a nie wersje globalne.
  • Nie mieszaj Number i BigInt w jednym wyrażeniu: konwertuj jawnie.

Dalej: null vs undefined

JavaScript ma dwa sposoby na powiedzenie "brak wartości", null i undefined, i nie są one wymienne. W następnej części: co oznacza każda z nich, czym się różnią i kiedy której używać.

Najczęściej zadawane pytania

Dlaczego 0.1 + 0.2 nie równa się 0.3 w JavaScript?

Bo typ Number w JavaScript to 64-bitowa liczba zmiennoprzecinkowa IEEE 754, a 0.1 i 0.2 nie da się dokładnie zapisać w systemie dwójkowym. Wynik to 0.30000000000000004. To nie jest błąd JavaScriptu: tak samo dzieje się w Pythonie, Javie i każdym innym języku, który używa tego formatu. Przy kwotach pieniędzy przeskaluj je do liczb całkowitych (groszy) albo użyj biblioteki do liczb dziesiętnych.

Czym jest BigInt w JavaScript i kiedy go używać?

BigInt to osobny typ prosty dla liczb całkowitych większych niż Number.MAX_SAFE_INTEGER (2^53 - 1). Tworzysz go, dopisując na końcu n, na przykład 9007199254740993n, albo przez BigInt(value). Używaj go do 64-bitowych identyfikatorów z bazy danych, kryptografii i wszelkich obliczeń na liczbach całkowitych, w których precyzja jest ważniejsza niż szybkość.

Czy można mieszać Number i BigInt w JavaScript?

Nie. 1n + 1 rzuca TypeError: Cannot mix BigInt and other types. Konwertuj jawnie przez BigInt(n) lub Number(b). Operatory porównania, takie jak < i ==, działają między tymi typami, ale === zwraca false, bo typy są różne.

Ilustracja języków programowania w Coddy

Ucz się programowania z Coddy

ZACZNIJ