解決JavaScript數字精度丟失問題的方法
一、JS數字精度丟失的一些典型問題
1. 大整數運算
9999999999999999==10000000000000001 //true
2. 兩個簡單的浮點數相加
0.1 + 0.2 != 0.3//false
///相減、相乘
0.18-1 //-0.8200000000000001
0.68*10 //6.800000000000001
3. toFixed 不會四舍五入
1.255.toFixed(2);//'1.25',不會四舍五入
1.225.toFixed(2);//'1.23',會四舍五入
二、JS 數字丟失精度的原因
- 進制轉換 :js 在做數字計算的時候,0.1 和 0.2 都會被轉成二進制后無限循環 ,但是 js 采用的 IEEE 754 二進制浮點運算,尾數最大可以存儲 53 位有效數字,於是大於 53 位后面的會全部截掉,將導致精度丟失。
- 雙精度存儲(double precision),占用 64 bit。(1位用來表示符號位,11位用來表示指數,52位表示尾數)
- 對階運算 :由於指數位數不相同,運算時需要對階運算,階小的尾數要根據階差來右移(
0舍1入
),尾數位移時可能會發生數丟失的情況,影響精度。
-
大整數的精度丟失和浮點數本質上是一樣的,尾數位最大是52位,因此 JS 中能精准表示的最大整數是 Math.pow(2, 53),十進制即 9007199254740992。
-
因此看似有窮的數字, 在計算機的二進制表示里卻是無窮的,由於存儲位數限制因此存在“舍去”,精度丟失就發生了
三、解決方案
1.大整數運算
- JavaScript 安全整數
Math.pow(-2, 53) ~Math.pow(2, 53)
,整數運算結果不超過 Math.pow(2, 53) 就不會丟失精度。
function getLen(num){
let numStr=num.toString()
console.log(numStr,numStr.indexOf('.')===-1)
return numStr.indexOf('.')===-1?0:numStr.split('.')[1].length
}
// bigRes 的大數相除(即 /)是會把小數部分截掉,不適合有小數點的
function add(a, b) {
const maxLen= Math.max(getLen(a),getLen(b));
const base=Math.pow(10,maxLen);
const bigA = BigInt(Math.round(base * a));
const bigB = BigInt(Math.round(base * b));
const bigRes=(bigA+bigB)/BigInt(base)
return Number(bigRes)
}
//小數會被截掉
1.1+0.11//1.2100000000000002
add(1.1,0.11);//0
Math.pow(2, 53) //9007199254740992
9007199254740992+1//9007199254740992
add(9007199254740992,1)//9007199254740993
2.小數運算
不能超過 Math.pow(-2, 53) ~Math.pow(2, 53)
- 解決方式:把小數放到位整數(乘倍數),再縮小回原來倍數(除倍數)
// 0.1 + 0.2
(0.1*10 + 0.2*10) / 10 == 0.3 // true
function myFixed(a, b) {//a:數字,b:小數點后有幾位數
return Math.round(a * Math.pow(10, b)) / Math.pow(10, b);
}
console.log(0.68*10); //6.800000000000001
console.log(myFixed(0.68*10, 1)); //6.8
- 封裝
/**
* floatObj 包含加減乘除四個方法,能確保浮點數運算不丟失精度
* 我們知道計算機編程語言里浮點數計算會存在精度丟失問題(或稱舍入誤差),其根本原因是二進制和實現位數限制有些數無法有限表示
* 以下是十進制小數對應的二進制表示
* 0.1 >> 0.0001 1001 1001 1001…(1001無限循環)
* 0.2 >> 0.0011 0011 0011 0011…(0011無限循環)
* 計算機里每種數據類型的存儲是一個有限寬度,比如 JavaScript 使用 64 位存儲數字類型,因此超出的會舍去。舍去的部分就是精度丟失的部分。
* ** method **
* add / subtract / multiply /divide
* ** explame **
* 0.1 + 0.2 == 0.30000000000000004 (多了 0.00000000000004)
* 19.9 * 100 == 1989.9999999999998 (少了 0.0000000000002)
* floatObj.add(0.1, 0.2) >> 0.3
* floatObj.multiply(19.9, 100) >> 1990
*
*/
var floatObj = function() {
/*
* 判斷obj是否為一個整數
*/
function isInteger(obj) {
return Math.floor(obj) === obj
}
/*
* 將一個浮點數轉成整數,返回小數點后面的長度。如 3.14 >> 314,2
* @param num {number} 小數
*/
function getLen(num){
let numStr=num.toString();
return numStr.indexOf('.')===-1?0:numStr.split('.')[1].length
}
/*
* 核心方法,實現加減乘除運算,確保不丟失精度
* 思路:把小數放大為整數(乘),進行算術運算,再縮小為小數(除)
* @param a {number} 運算數1
* @param b {number} 運算數2
* @param digits {number} 精度,保留的小數點數,比如 2, 即保留為兩位小數
* @param op {string} 運算類型,有加減乘除(add/subtract/multiply/divide)
*/
function operation(a, b, digits, op) {
var result = null
const maxLen= Math.max(getLen(a),getLen(b));
const base=Math.pow(10,maxLen);
const bigA = Math.round(base * a);
const bigB = Math.round(base * b);
switch (op) {
case 'add':
result = bigA + bigB
break
case 'subtract':
result = bigA - bigB
break
case 'multiply':
result = bigA * bigB
break
case 'divide':
result = bigA / bigB
break
}
return result / base
}
// 加減乘除的四個接口
function add(a, b, digits) {
return operation(a, b, digits, 'add')
}
function subtract(a, b, digits) {
return operation(a, b, digits, 'subtract')
}
function multiply(a, b, digits) {
return operation(a, b, digits, 'multiply')
}
function divide(a, b, digits) {
return operation(a, b, digits, 'divide')
}
// exports
return {
add,
subtract,
multiply,
divide
}
}();
floatObj.add(0.1, 0.2,3)//0.3
floatObj.add(1.1,0.11);//1.21
floatObj.multiply(19.9, 100) //1990
3.toFixed的修復
function toFixedFun (data, len){
// debugger
const number = Number(data);
if (isNaN(number) || number >= Math.pow(10, 21)) {
return number.toString();
}
if (typeof (len) === 'undefined' || len === 0) {
return (Math.round(number)).toString();
}
let result = number.toString();
const numberArr = result.split('.');
if (numberArr.length < 2) {
// 整數的情況
return padNum(result);
}
const intNum = numberArr[0]; // 整數部分
const deciNum = numberArr[1];// 小數部分
const lastNum = deciNum.substr(len, 1);// 最后一個數字
if (deciNum.length === len) {
// 需要截取的長度等於當前長度
return result;
}
if (deciNum.length < len) {
// 需要截取的長度大於當前長度 1.3.toFixed(2)
return padNum(result);
}
// 需要截取的長度小於當前長度,需要判斷最后一位數字
result = `${intNum}.${deciNum.substr(0, len)}`;
if (parseInt(lastNum, 10) >= 5) {
// 最后一位數字大於5,要進位
const times = Math.pow(10, len); // 需要放大的倍數
let changedInt = Number(result.replace('.', ''));// 截取后轉為整數
changedInt++; // 整數進位
changedInt /= times;// 整數轉為小數,注:有可能還是整數
result = padNum(`${changedInt }`);
}
return result;
// 對數字末尾加0
function padNum(num) {
const dotPos = num.indexOf('.');
if (dotPos === -1) {
// 整數的情況
num += '.';
for (let i = 0; i < len; i++) {
num += '0';
}
return num;
} else {
// 小數的情況
const need = len - (num.length - dotPos - 1);
for (let j = 0; j < need; j++) {
num += '0';
}
return num;
}
}
}
toFixedFun(1.255,2);//1.26
四、三方庫
1.Math.js
-
專門為 JavaScript 和 Node.js 提供的一個廣泛的數學庫。支持數字,大數字(超出安全數的數字),復數,分數,單位和矩陣。 功能強大,易於使用。
-
官網:mathjs.org/
-
GitHub:github.com/josdejong/m…
2.big.js
參考轉自: