JavaScript für ungeduldige Programmierer (ES2022-Ausgabe)
Bitte unterstützen Sie dieses Buch: kaufen Sie es oder spenden Sie
(Werbung, bitte nicht blockieren.)

18 Bigints – Ganzzahlen mit beliebiger Genauigkeit [ES2020] (Fortgeschritten)



In diesem Kapitel befassen wir uns mit Bigints, den Ganzzahlen in JavaScript, deren Speicherplatz nach Bedarf wächst und schrumpft.

18.1 Warum Bigints?

Vor ECMAScript 2020 behandelte JavaScript Ganzzahlen wie folgt:

Manchmal benötigen wir mehr als 53 vorzeichenbehaftete Bits – zum Beispiel:

18.2 Bigints

Bigint ist ein neuer primitiver Datentyp für Ganzzahlen. Bigints haben keine feste Speichergröße in Bits; ihre Größe passt sich an die dargestellten Ganzzahlen an.

Ein Bigint-Literal ist eine Abfolge von einer oder mehreren Ziffern, gefolgt von einem n – zum Beispiel:

123n

Operatoren wie - und * sind überladen und funktionieren mit Bigints.

> 123n * 456n
56088n

Bigints sind primitive Werte. typeof gibt für sie ein neues Ergebnis zurück:

> typeof 123n
'bigint'

18.2.1 Größer als 53 Bits für Ganzzahlen

JavaScript-Zahlen werden intern als Bruch multipliziert mit einem Exponenten dargestellt (siehe §16.8 „Hintergrund: Gleitkomma-Genauigkeit“ für Details). Infolgedessen gibt es, wenn wir über die höchste sichere Ganzzahl 253−1 hinausgehen, immer noch einige Ganzzahlen, die dargestellt werden können, aber mit Lücken dazwischen.

> 2**53 - 2 // safe
9007199254740990
> 2**53 - 1 // safe
9007199254740991

> 2**53 // unsafe, same as next integer
9007199254740992
> 2**53 + 1
9007199254740992
> 2**53 + 2
9007199254740994
> 2**53 + 3
9007199254740996
> 2**53 + 4
9007199254740996
> 2**53 + 5
9007199254740996

Bigints ermöglichen es uns, über 53 Bits hinauszugehen.

> 2n**53n
9007199254740992n
> 2n**53n + 1n
9007199254740993n
> 2n**53n + 2n
9007199254740994n

18.2.2 Beispiel: Bigints verwenden

So sieht die Verwendung von Bigints aus (Code basierend auf einem Beispiel im Vorschlag):

/**
 * Takes a bigint as an argument and returns a bigint
 */
function nthPrime(nth) {
  if (typeof nth !== 'bigint') {
    throw new TypeError();
  }
  function isPrime(p) {
    for (let i = 2n; i < p; i++) {
      if (p % i === 0n) return false;
    }
    return true;
  }
  for (let i = 2n; ; i++) {
    if (isPrime(i)) {
      if (--nth === 0n) return i;
    }
  }
}

assert.deepEqual(
  [1n, 2n, 3n, 4n, 5n].map(nth => nthPrime(nth)),
  [2n, 3n, 5n, 7n, 11n]
);

18.3 Bigint-Literale

Ähnlich wie Zahlenliterale unterstützen Bigint-Literale mehrere Basen:

Negative Bigints werden durch Voranstellen des unären Minuszeichens erzeugt: -0123n

18.3.1 Unterstriche (_) als Trennzeichen in Bigint-Literalen [ES2021]

Genau wie bei Zahlenliteralen können wir Unterstriche (_) als Trennzeichen in Bigint-Literalen verwenden.

const massOfEarthInKg = 6_000_000_000_000_000_000_000_000n;

Bigints werden oft zur Darstellung von Geld im Finanzsektor verwendet. Trennzeichen können auch hier helfen:

const priceInCents = 123_000_00n; // 123 thousand dollars

Wie bei Zahlenliteralen gelten zwei Einschränkungen:

18.4 Wiederverwendung von Zahlenoperatoren für Bigints (Überladung)

Bei den meisten Operatoren dürfen wir Bigints und Zahlen nicht mischen. Wenn wir es doch tun, werden Ausnahmen ausgelöst:

> 2n + 1
TypeError: Cannot mix BigInt and other types, use explicit conversions

Der Grund für diese Regel ist, dass es keine allgemeine Möglichkeit gibt, eine Zahl und ein Bigint in einen gemeinsamen Typ zu konvertieren: Zahlen können keine Bigints über 53 Bits hinaus darstellen, Bigints können keine Brüche darstellen. Daher warnen die Ausnahmen vor Tippfehlern, die zu unerwarteten Ergebnissen führen können.

Sollte das Ergebnis des folgenden Ausdrucks beispielsweise 9007199254740993n oder 9007199254740992 sein?

2**53 + 1n

Auch ist nicht klar, was das Ergebnis des folgenden Ausdrucks sein sollte:

2n**53n * 3.3

18.4.1 Arithmetische Operatoren

Binäres +, binäres -, *, ** funktionieren wie erwartet.

> 7n * 3n
21n

Das Mischen von Bigints und Strings ist in Ordnung.

> 6n + ' apples'
'6 apples'

/, % runden in Richtung Null (wie Math.trunc()).

> 1n / 2n
0n

Das unäre - funktioniert wie erwartet.

> -(-64n)
64n

Das unäre + wird für Bigints nicht unterstützt, da viel Code darauf angewiesen ist, dass es seinen Operanden in eine Zahl konvertiert.

> +23n
TypeError: Cannot convert a BigInt value to a number

18.4.2 Vergleichsoperatoren

Vergleichsoperatoren <, >, >=, <= funktionieren wie erwartet.

> 17n <= 17n
true
> 3n > -1n
true

Das Vergleichen von Bigints und Zahlen birgt keine Risiken. Daher können wir Bigints und Zahlen mischen.

> 3n > -1
true

18.4.3 Bitweise Operatoren

18.4.3.1 Bitweise Operatoren für Zahlen

Bitweise Operatoren interpretieren Zahlen als 32-Bit-Ganzzahlen. Diese Ganzzahlen sind entweder vorzeichenlos oder vorzeichenbehaftet. Wenn sie vorzeichenbehaftet sind, ist die Negation einer Ganzzahl ihr Zweierkomplement (das Addieren einer Ganzzahl zu ihrem Zweierkomplement – unter Ignorieren von Überläufen – ergibt Null).

> 2**32-1 >> 0
-1

Aufgrund dieser festen Größe der Ganzzahlen geben ihre höchsten Bits ihr Vorzeichen an.

> 2**31 >> 0 // highest bit is 1
-2147483648
> 2**31 - 1 >> 0 // highest bit is 0
2147483647
18.4.3.2 Bitweise Operatoren für Bigints

Für Bigints interpretieren bitweise Operatoren ein negatives Vorzeichen als unendliches Zweierkomplement – zum Beispiel:

Das heißt, ein negatives Vorzeichen ist eher ein externes Flag und nicht als tatsächliches Bit repräsentiert.

18.4.3.3 Bitweise Nicht (~)

Bitweise Nicht (~) invertiert alle Bits.

> ~0b10n
-3n
> ~0n
-1n
> ~-2n
1n
18.4.3.4 Binäre bitweise Operatoren (&, |, ^)

Die Anwendung binärer bitweiser Operatoren auf Bigints funktioniert analog zur Anwendung auf Zahlen.

> (0b1010n |  0b0111n).toString(2)
'1111'
> (0b1010n &  0b0111n).toString(2)
'10'

> (0b1010n | -1n).toString(2)
'-1'
> (0b1010n & -1n).toString(2)
'1010'
18.4.3.5 Bitweise vorzeichenbehaftete Shift-Operatoren (<< und >>)

Die vorzeichenbehafteten Shift-Operatoren für Bigints erhalten das Vorzeichen einer Zahl.

> 2n << 1n
4n
> -2n << 1n
-4n

> 2n >> 1n
1n
> -2n >> 1n
-1n

Erinnern Sie sich, dass -1n eine unendlich nach links reichende Sequenz von Einsen ist. Deshalb ändert sich beim Linksshift nichts.

> -1n >> 20n
-1n
18.4.3.6 Bitweise vorzeichenlose Rechtsshift-Operator (>>>)

Es gibt keinen vorzeichenlosen Rechtsshift-Operator für Bigints.

> 2n >>> 1n
TypeError: BigInts have no unsigned right shift, use >> instead

Warum? Die Idee hinter dem vorzeichenlosen Rechtsshift ist, dass von „links“ eine Null hineingeschoben wird. Anders ausgedrückt, es wird angenommen, dass es eine endliche Anzahl von Binärziffern gibt.

Bei Bigints gibt es jedoch kein „links“, ihre Binärziffern erstrecken sich unendlich. Dies ist besonders bei negativen Zahlen wichtig.

Der vorzeichenbehaftete Rechtsshift funktioniert auch bei einer unendlichen Anzahl von Ziffern, da die höchste Ziffer erhalten bleibt. Daher kann er an Bigints angepasst werden.

18.4.4 Ungenauer Vergleich (==) und Ungleichheit (!=)

Ungenauer Vergleich (==) und Ungleichheit (!=) konvertieren Werte.

> 0n == false
true
> 1n == true
true

> 123n == 123
true

> 123n == '123'
true

18.4.5 Genauer Vergleich (===) und Ungleichheit (!==)

Genauer Vergleich (===) und Ungleichheit (!==) betrachten Werte nur dann als gleich, wenn sie denselben Typ haben.

> 123n === 123
false
> 123n === 123n
true

18.5 Der Wrapper-Konstruktor BigInt

Analog zu Zahlen gibt es für Bigints den zugehörigen Wrapper-Konstruktor BigInt.

18.5.1 BigInt als Konstruktor und als Funktion

Tabelle 13: Konvertierung von Werten in Bigints.
x BigInt(x)
undefined Löst TypeError aus
null Löst TypeError aus
boolean false 0n, true 1n
number Beispiel: 123 123n
Nicht-Ganzzahl löst RangeError aus
bigint x (keine Änderung)
string Beispiel: '123' 123n
Nicht parsenbar löst SyntaxError aus
symbol Löst TypeError aus
object Konfigurierbar (z.B. über .valueOf())
18.5.1.1 Konvertierung von undefined und null

Ein TypeError wird ausgelöst, wenn x entweder undefined oder null ist.

> BigInt(undefined)
TypeError: Cannot convert undefined to a BigInt
> BigInt(null)
TypeError: Cannot convert null to a BigInt
18.5.1.2 Konvertierung von Strings

Wenn ein String keine Ganzzahl darstellt, löst BigInt() einen SyntaxError aus (während Number() den Fehlerwert NaN zurückgibt).

> BigInt('abc')
SyntaxError: Cannot convert abc to a BigInt

Das Suffix 'n' ist nicht erlaubt.

> BigInt('123n')
SyntaxError: Cannot convert 123n to a BigInt

Alle Basen von Bigint-Literalen sind erlaubt.

> BigInt('123')
123n
> BigInt('0xFF')
255n
> BigInt('0b1101')
13n
> BigInt('0o777')
511n
18.5.1.3 Nicht-Ganzzahlige Zahlen erzeugen Ausnahmen
> BigInt(123.45)
RangeError: The number 123.45 cannot be converted to a BigInt because
it is not an integer
> BigInt(123)
123n
18.5.1.4 Konvertierung von Objekten

Wie Objekte in Bigints konvertiert werden, kann konfiguriert werden – zum Beispiel durch Überschreiben von .valueOf().

> BigInt({valueOf() {return 123n}})
123n

18.5.2 Methoden von BigInt.prototype.*

BigInt.prototype enthält die Methoden, die von primitiven Bigints „geerbt“ werden.

18.5.3 Methoden von BigInt.*

18.5.4 Casting und 64-Bit-Ganzzahlen

Casting ermöglicht es uns, Ganzzahlwerte mit einer bestimmten Anzahl von Bits zu erstellen. Wenn wir uns auf nur 64-Bit-Ganzzahlen beschränken wollen, müssen wir immer casten.

const uint64a = BigInt.asUintN(64, 12345n);
const uint64b = BigInt.asUintN(64, 67890n);
const result = BigInt.asUintN(64, uint64a * uint64b);

18.6 Umwandlung von Bigints in andere primitive Typen

Diese Tabelle zeigt, was passiert, wenn wir Bigints in andere primitive Typen konvertieren.

Konvertierung nach Explizite Konvertierung Implizite Konvertierung
boolean Boolean(0n) false !0n true
Boolean(int) true !int false
number Number(7n) 7 (Beispiel) +int TypeError (1)
string String(7n) '7' (Beispiel) ''+7n '7' (Beispiel)

Fußnote

18.7 TypedArrays und DataView-Operationen für 64-Bit-Werte

Dank Bigints können Typed Arrays und DataViews 64-Bit-Werte unterstützen.

18.8 Bigints und JSON

Der JSON-Standard ist fixiert und wird sich nicht ändern. Der Vorteil ist, dass alter JSON-Parsing-Code niemals veraltet sein wird. Der Nachteil ist, dass JSON nicht erweitert werden kann, um Bigints zu enthalten.

Das Stringifizieren von Bigints löst Ausnahmen aus.

> JSON.stringify(123n)
TypeError: Do not know how to serialize a BigInt
> JSON.stringify([123n])
TypeError: Do not know how to serialize a BigInt

18.8.1 Stringifizieren von Bigints

Daher ist unsere beste Option, Bigints in Strings zu speichern.

const bigintPrefix = '[[bigint]]';

function bigintReplacer(_key, value) {
  if (typeof value === 'bigint') {
    return bigintPrefix + value;
  }
  return value;
}

const data = { value: 9007199254740993n };
assert.equal(
  JSON.stringify(data, bigintReplacer),
  '{"value":"[[bigint]]9007199254740993"}'
);

18.8.2 Parsen von Bigints

Der folgende Code zeigt, wie Strings wie derjenige, den wir im vorherigen Beispiel erzeugt haben, geparst werden.

function bigintReviver(_key, value) {
  if (typeof value === 'string' && value.startsWith(bigintPrefix)) {
    return BigInt(value.slice(bigintPrefix.length));
  }
  return value;
}

const str = '{"value":"[[bigint]]9007199254740993"}';
assert.deepEqual(
  JSON.parse(str, bigintReviver),
  { value: 9007199254740993n }
);

18.9 FAQ: Bigints

18.9.1 Wann soll ich Zahlen und wann Bigints verwenden?

Meine Empfehlungen:

Alle vorhandenen Web-APIs geben nur Zahlen zurück und akzeptieren nur Zahlen und werden nur von Fall zu Fall auf Bigint umgestellt.

18.9.2 Warum nicht einfach die Genauigkeit von Zahlen so erhöhen wie bei Bigints?

Man könnte theoretisch number in integer und double aufteilen, aber das würde der Sprache viele neue Komplexitäten hinzufügen (mehrere reine Integer-Operatoren usw.). Ich habe die Konsequenzen in einem Gist skizziert.


Danksagungen