Zahlen in JavaScript werden gemäß dem IEEE 754-Standard im 64-Bit-Gleitkommaformat mit doppelter Genauigkeit dargestellt. Darin belegen das Vorzeichenbit S, die Exponentenbits E und die Mantissenbits M jeweils 1, 11 bzw. 52 Bits, und in derES5-Spezifikationwird darauf hingewiesen, dass der Wertebereich der Exponentenbits E ist[-1074, 971]。


Zusammenfassung der Präzisionsprobleme

Offensichtlich ist es unmöglich, unendliche Zahlen mit endlichen Bits darzustellen, sodass eine Reihe von Präzisionsproblemen entsteht:

  • Gleitkomma-Präzisionsprobleme, wie zum Beispiel0.1 + 0.2 !== 0.3
  • Probleme mit der Präzision großer Zahlen, wie zum Beispiel9999 9999 9999 9999 == 1000 0000 0000 0000 1
  • Ungenaue toFixed-Rundungsergebnisse, wie zum Beispiel1.335.toFixed(2) == 1.33

Gleitkomma-Präzision und toFixed gehören tatsächlich zur selben Problemkategorie; beide werden durch die Unfähigkeit verursacht, Gleitkommazahlen präzise darzustellen, wie folgt:

(1.335).toPrecision(20);    // "1.3349999999999999645"

Was das Präzisionsproblem großer Zahlen betrifft, können wir zunächst den folgenden Codeausschnitt betrachten:

// 能精确表示的整数范围上限,S为1个0,E为11个0,S为52个1
Math.pow(2, 53) - 1 === Number.MAX_SAFE_INTEGER    // true
// 能精确表示的整数范围下限,S为1个1,E为11个0,S为52个1
-(Math.pow(2, 53) - 1) === Number.MIN_SAFE_INTEGER    // true
// 能表示的最大数字,S为1个0,E为971,S为52个1
(Math.pow(2, 53) - 1) * Math.pow(2, 971) === Number.MAX_VALUE    // true
// 能表示的最接近于0的正数,S为1个0,E为-1074,S为0
Math.pow(2, -1074) === Number.MIN_VALUE // true

Aus dem Obigen können wir verstehen, dass[MIN_SAFE_INTEGER, MAX_SAFE_INTEGER]Ganzzahlen innerhalb dieses Bereichs präzise dargestellt werden können, aber Ganzzahlen außerhalb dieses Bereichs nicht unbedingt präzise dargestellt werden können. Dies führt zu dem sogenannten Problem des Präzisionsverlusts bei großen Zahlen.

Lösungsansätze

Betrachten wir zunächst, wie das Präzisionsproblem der Gleitkomma-Arithmetik gelöst werden kann. Es gibt 3 Ansätze:

  • Wenn man bedenkt, dass die Abweichung bei jeder Gleitkommaoperation sehr klein ist (tatsächlich ist sie es nicht), kann man das Ergebnis auf eine festgelegte Genauigkeit runden, zum Beispiel kann manparseFloat(result.toFixed(12));
  • Konvertieren Sie Gleitkommazahlen in Ganzzahlen zur Berechnung und teilen Sie dann das Ergebnis. Zum Beispiel kann 0.1 + 0.2 umgewandelt werden zu(1*2)/3。
  • Konvertieren Sie Gleitkommazahlen in Zeichenketten und simulieren Sie den tatsächlichen Berechnungsprozess.

Betrachten wir zunächst den ersten Ansatz. In den meisten Fällen kann er korrekte Ergebnisse liefern, aber für einige Extremfälle reicht toFixed auf 12 Stellen nicht aus, zum Beispiel:

210000 * 10000  * 1000 * 8.2    // 17219999999999.998
parseFloat(17219999999999.998.toFixed(12));    // 17219999999999.998,而正确结果为 17220000000000

Für die obigen Situationen gilt: Wenn das Ergebnis korrekt sein soll, benötigen SietoFixed(2), was offensichtlich nicht akzeptabel ist.

Betrachten wir nun den zweiten Ansatz, zum Beispielnumber-precisionDiese Bibliothek verwendet diesen Ansatz, hat aber ebenfalls Probleme, zum Beispiel:

// 这两个浮点数,转化为整数之后,相乘的结果已经超过了 MAX_SAFE_INTEGER
123456.789 * 123456.789     // 转化为 (123456789 * 123456789)/1000000,结果是 15241578750.19052

Daher ziehen wir letztendlich in Betracht, den dritten Ansatz zu verwenden. Derzeit gibt es bereits viele ausgereifte Bibliotheken, wie zum Beispielbignumber.js,decimal.js, undbig.jsusw. Wir können die entsprechenden Tools je nach Bedarf auswählen. Darüber hinaus lösen diese Bibliotheken nicht nur die Genauigkeitsprobleme der Gleitkomma-Arithmetik, sondern unterstützen auch Operationen mit großen Zahlen und beheben die ungenauen Ergebnisse des nativen toFixed.


Am Rande

Es gibt ein weiteres Problem im Zusammenhang mit der Berechnung in JavaScript, nämlichMath.round(x), obwohl es keine Genauigkeitsprobleme verursacht, hat es eine kleine Fallgrube, die leicht zu übersehen ist. Im Folgenden wird seine Rundungsstrategie beschrieben:

  • Wenn der Bruchteil größer als 0.5 ist, wird auf die nächste ganze Zahl mit einem größeren Absolutwert gerundet.
  • Wenn der Bruchteil kleiner als 0.5 ist, wird auf die nächste ganze Zahl mit einem kleineren Absolutwert gerundet.
  • Wenn der Bruchteil gleich 0.5 ist, dannwird zur nächsten ganzen Zahl in Richtung positive Unendlichkeit gerundet。

Daher ist das Ergebnis für Math.round(-1.5) -1, was möglicherweise nicht das Ergebnis ist, das wir wollen.

Natürlich bieten big.js und die anderen oben genannten Bibliotheken alle ihre eigenen round-Funktionen und ermöglichen die Angabe von Rundungsregeln, um dieses Problem zu vermeiden.

Originaladresse: https://xwjgo.github.io/2018/03/17/js%E4%B8%AD%E7%B2%BE%E5%BA%A6%E9%97%AE%E9%A2%98%E5%8F%8A%E8%A7%A3%E5%86%B3%E6%96%B9%E6%A1%88/