JavaScript 賭運氣的四捨五入

weixin_33861800發表於2018-04-17

起源於線上一個BUG,計算值與後端java 的 BigDecimal計算值不匹配,追蹤資料,在tofixed 和 round 的時候資料不精準

10.135.toFixed(2) //10.13
複製程式碼

這就很不友好了啊,想一下解決方案,首先 先要知道為啥

  • 如果引數的小數部分恰好等於0.5,則舍入到下一個在正無窮(+∞)方向上的整數。注意,與很多其他語言中的round()函式不同,Math.round()並不總是舍入到遠離0的方向(尤其是在負數的小數部分恰好等於0.5的情況下)--MDN

這種情勢是為啥呢,有經驗的都知道 ,肯定是進位制的鍋了啊

先看一下儲存結構

ECMA-262 遵循 IEEE 754 規範,採用雙精度儲存(double precision),佔用 64 bit

儲存結構中可以看出, 指數部分的長度是11個二進位制,即指數部分能表示的最大值是 2047(211-1),取中間值進行偏移,用來表示負指數,也就是說指數的範圍是 [-1023,1024] 。因此,這種儲存結構能夠表示的數值範圍為 21024 到 2-1023 ,超出這個範圍的數無法表示 。21024 轉換為科學計數法如下所示:

21024  = 1.7976931348623157 × 10308
複製程式碼

因此,JavaScript 中能表示的最大值是 1.7976931348623157e+308,最小值為 5e-324 。

這兩個邊界值可以分別通過訪問 Number 物件的 MAX_VALUE 屬性和 MIN_VALUE 屬性來獲取:

Number.MAX_VALUE; //1.7976931348623157e+308
Number.MIN_VALUE; //5e-324
複製程式碼

如果數字超過最大值或最小值,JavaScript 將返回一個不正確的值,這稱為正向溢位(overflow) 或 負向溢位(underflow) 。

Number.MAX_VALUE+1 == Number.MAX_VALUE; //true
Number.MAX_VALUE+1e292; //Infinity
Number.MIN_VALUE + 1; //1
Number.MIN_VALUE - 3e-324; //0
Number.MIN_VALUE - 2e-324; //5e-324
複製程式碼

然後數值精度就知道了

在 64 位的二進位制中,符號位決定了一個數的正負,指數部分決定了數值的大小,小數部分決定了數值的精度。

IEEE754 規定,有效數字第一位預設總是1 。因此,在表示精度的尾數前面,還存在一個隱藏位 ,固定為 1 ,但它不儲存在 64 位浮點數之中。也就是說,有效數字總是 1.xx...xx 的形式,其中 xx..xx 的部分儲存在 64 位浮點數之中,最長為52位 。所以,JavaScript 提供的有效數字最長為 53 個二進位制位,其內部實際的表現形式為:

(-1)^符號位 * 1.xx...xx * 2^指數位

這意味著,JavaScript 能表示並進行精確算術運算的整數範圍為:[-253-1,253-1],即從最小值 -9007199254740991 到最大值 9007199254740991 之間的範圍 。

Math.pow(2, 53)-1 ; // 9007199254740991
-Math.pow(2, 53)-1 ; // -9007199254740991
複製程式碼

可以通過 Number.MAX_SAFE_INTEGER 和 Number.MIN_SAFE_INTEGER 來分別獲取這個最大值和最小值。

console.log(Number.MAX_SAFE_INTEGER) ; // 9007199254740991
console.log(Number.MIN_SAFE_INTEGER) ; // -9007199254740991
複製程式碼

對於超過這個範圍的整數,JavaScript 依舊可以進行運算,但卻不保證運算結果的精度。

Math.pow(2, 53) ; // 9007199254740992
Math.pow(2, 53) + 1; // 9007199254740992
9007199254740993; //9007199254740992
90071992547409921; //90071992547409920
0.923456789012345678;//0.9234567890123456`
複製程式碼

然後我們再看一下為啥會精度丟失

計算機中的數字都是以二進位制儲存的,如果要計算 0.1 + 0.2 的結果,計算機會先把 0.1 和 0.2 分別轉化成二進位制,然後相加,最後再把相加得到的結果轉為十進位制 。

但有一些浮點數在轉化為二進位制時,會出現無限迴圈 。比如, 十進位制的 0.1 轉化為二進位制,會得到如下結果:

0.0001 1001 1001 1001 1001 1001 1001 1001 …(1001無限迴圈) 
複製程式碼

而儲存結構中的尾數部分最多隻能表示 53 位。為了能表示 0.1,只能模仿十進位制進行四捨五入了,但二進位制只有 0 和 1 , 於是變為 0 舍 1 入 。 因此,0.1 在計算機裡的二進位制表示形式如下:

0.0001100110011001100110011001100110011001100110011001101
複製程式碼

用標準計數法表示如下:

(−1)0 × 2−4 × (1.1001100110011001100110011001100110011001100110011010)2
複製程式碼

同樣,0.2 的二進位制也可以表示為:

(−1)0 × 2−3 × (1.1001100110011001100110011001100110011001100110011010)2 
複製程式碼

在計算浮點數相加時,需要先進行對位,將較小的指數化為較大的指數,並將小數部分相應右移:

0.1→ (−1)0 × 2−3 × (0.11001100110011001100110011001100110011001100110011010)2
0.2→ (−1)0 × 2−3 × (1.1001100110011001100110011001100110011001100110011010)2
複製程式碼

最終,0.1 + 0.2 在計算機裡的計算過程如下:

經過上面的計算過程,0.1 + 0.2 得到的結果也可以表示為:

(−1)0 × 2−2 × (1.0011001100110011001100110011001100110011001100110100)2
複製程式碼

然後,通過 JS 將這個二進位制結果轉化為十進位制表示:

(-1)**0 * 2**-2 * (0b10011001100110011001100110011001100110011001100110100 * 2**-52); //0.30000000000000004
console.log(0.1 + 0.2) ; // 0.30000000000000004
複製程式碼

這是一個典型的精度丟失案例,從上面的計算過程可以看出,0.1 和 0.2 在轉換為二進位制時就發生了一次精度丟失,而對於計算後的二進位制又有一次精度丟失 。因此,得到的結果是不準確的。 2.5 特殊數值 JavaScript 提供了幾個特殊數值,用於判斷數字的邊界和其他特性 。如下所示:

Number.MAX_VALUE:JavaScript 中的最大值
Number.MIN_VALUE:JavaScript 中的最小值
Number.MAX_SAFE_INTEGER:最大安全整數,為 253-1
Number.MIN_SAFE_INTEGER:最小安全整數,為 -(253-1)
Number.POSITIVE_INFINITY:對應 Infinity,代表正無窮
Number.NEGATIVE_INFINITY:對應 -Infinity,代表負無窮
Number.EPSILON:是一個極小的值,用於檢測計算結果是否在誤差範圍內
Number.NaN:表示非數字,NaN與任何值都不相等,包括NaN本身
Infinity:表示無窮大,分 正無窮 Infinity 和 負無窮 -Infinity
複製程式碼

如何進行數值轉換

有 3 個函式可以把非數值轉換為數值: Number()、parseInt() 和 parseFloat()。Number() 可以用於任何資料型別,而另兩個函式則專門用於把字串轉換成數值。

對於字串而言,Number() 只能對字串進行整體轉換,而 parseInt() 和 parseFloat() 可以對字串進行部分轉換,即只轉換第一個無效字元之前的字元。

對於不同資料型別的轉換,Number() 的處理也不盡相同,其轉換規則如下:

【1】如果是 Boolean 值,true 和 false 將分別被轉換為 1 和 0。

【2】如果是數字值,只是簡單的傳入和返回。

【3】如果是 null 值,返回 0。

【4】如果是 undefined,返回 NaN。

【5】如果是字串,遵循下列規則: 如果字串中只包含數字(包括前面帶正號或負號的情況),則將其轉換為十進位制數值; 如果字串中包含有效的浮點格式,則將其轉換為對應的浮點數值; 如果字串中包含有效的十六進位制格式,則將其轉換為相同大小的十進位制整數值; 如果字串是空的(不包含任何字元),則將其轉換為 0; 如果字串中包含除上述格式之外的字元,則將其轉換為 NaN。 【6】如果是物件,則呼叫物件的 valueOf() 方法,然後依照前面的規則轉換返回的值。如果轉換的結果是 NaN,則呼叫物件的 toString() 方法,然後再次依照前面的規則轉換返回的字串值。

注意:一元加操作符[+] 和 Number() 具有同樣的作用。

速度更快精度更差的位運算

位操作符用於在最基本的層次上,即按記憶體中表示數值的位來運算元值。ECMAScript 中的所有數值都以 IEEE754 64 位格式儲存,但位操作符並不直接操作 64 位的值。而是先將 64 位的值轉換成 32 位的整數,然後執行操作,最後再將結果轉換回 64 位。常見的位運算有以下幾種:

按位非(NOT):~ 

按位與(AND):& 

按位或(OR): |

按位異或(XOR):^

左移:<<

有符號右移:>>

無符號右移:>>>

複製程式碼

四捨五入怎麼才能好用

首先我們先看一下資料是怎麼存的 ECMA-262 只需要最多 21 位顯示數字。

0.135.toPrecision(21)  //"10.1349999999999997868"  每個人可能不一樣。。
複製程式碼

問題到這裡 基本就明瞭了,怎麼解決呢,上程式碼。。

    /**
     * 四捨五入
     * @param number 要四捨五入的數字
     * @param precision 精度 保留小數點位數
     * @returns {*}
     */
    function round(number,precision) {
      const enlargeDigits = function enlargeDigits(times) {
        return function (number) {
          return +(String(number) + "e" + String(times));
        };
      };
      const toFixed = function toFixed(precision) {
        return function (number) {
          return number.toFixed(precision);
        };
      };
      const compose = function compose() {
        for (var _len = arguments.length, functions = Array(_len), _key = 0; _key < _len; _key++) {
          functions[_key] = arguments[_key];
        }

        var nonFunctionTypeLength = functions.filter(function (item) {
          return typeof item !== 'function';
        }).length;
        if (nonFunctionTypeLength > 0) {
          throw new Error("compose's params must be functions");
        }
        if (functions.length === 0) {
          return function (arg) {
            return arg;
          };
        }
        if (functions.length === 1) {
          return functions[0];
        }
        return functions.reduce(function (a, b) {
          return function () {
            return a(b.apply(undefined, arguments));
          };
        });
      };
      var precision = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 2;

      if (Number.isNaN(+number)) {
        throw new Error("number's type must be Number");
      }
      if (Number.isNaN(+precision)) {
        throw new Error("precision's type must be Number");
      }
      return compose(toFixed(precision), enlargeDigits(-precision), Math.round, enlargeDigits(precision))(number)
    }
複製程式碼

相關文章