解決JavaScript數字精度丟失問題的方法


解決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

參考轉自:

解決JavaScript數字精度丟失問題的方法

0.1 + 0.2不等於0.3?為什么JavaScript有這種“騷”操作?


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM