你可能需要一個四捨五入的工具函數

語言: CN / TW / HK

       

目前存在什麼問題

問題:toFixed函數可以滿足一部分小數的四捨五入,首先可以看下mdn對於 Number.prototype.toFixed() [1] 的定義。

mdn的例子也有這個例子

2.55.toFixed(1)      // 返回 '2.5'. Note it rounds down - see warning above

警告:浮點數不能精確地用二進制表示所有小數。這可能會導致意外的結果,例如 0.1 + 0.2 === 0.3 返回  false .

mdn的説法是 浮點數的小數計算會出現異常。因此toFixed函數並不能滿足嚴格意義上的四捨五入。

為什麼不使用下面的方法進行四捨五入

const round = (num: number, decimal = 2): string => {
const rate = 10 ** decimal;
const temp = Math.round(num * rate) / rate;
let strNum = String(temp);
const numArr = strNum.split('.');
if (!numArr[1]) {
strNum += '.';
strNum = strNum.padEnd(strNum.length + decimal, '0');
} else if (numArr[1].length < decimal) {
strNum = strNum.padEnd(numArr[0].length + 1 + decimal, '0');
}
return strNum;
};

這樣處理的核心代碼是 Math.round(num * 10 ** decimal;) / 10 ** decimal;

其實這個可以滿足大部分場景,但仍然有兩個小問題:

  1. 如果num本身沒超過 Number.MAX_SAFE_INTEGER 但是 乘以 rate 以後超過了,則可能又會發生一些意料之外的case

  2. 對於某些場景還是無法處理,如 1.255 保留兩位小數,主要原因是也發生了精度丟失。

0.1 + 0.2 !== 0.3 的具體原因

JavaScript 中所有數字包括整數和小數都只有一種類型 — Number。它的實現遵循 IEEE 754 標準,使用 64 位固定長度來表示,也就是標準的 double 雙精度浮點數。

整個計算過程要經歷以下幾個步驟:

十進制轉二進制

先把0.1轉換為二進制,見下圖:

這個處理過程是一個無限循環的狀態, 可以在這直接查看結果 [2] ,最後結果是0.0001100110011001100110011001100110011001100110011001101...

0011 將會無限循環

二進制轉科學記數法

1.1(0011)… * 2^-4(小數點向右移4位,二進制中底數為2)

對科學記數法數據的二進制表示

64位存儲科學記數法

第一位是符號位,0是正數,1是負數,(-1的0次方還是1次方),case裏就是 0

其後的11位(指數部分)用於存儲科學記數法中指數的二進制數,11位的存儲範圍是 Math.pow(2, 11), 即2048,其中以1023作為正負分界線,這個case裏,-4 就是 1023-4 = 1019,轉換成二進制後為:01111111011

剩餘的52位(尾數部分)用於存儲科學記數法中尾數小數點後52位

所以0.1的二進制是

0 01111111011 1001100110011001100110011001100110011001100110011010

同理0.2的二進制是

0 01111111100 1001100110011001100110011001100110011001100110011010

對階運算

0.1的指數是-4,0.2的指數是-3。要想將他們運算的結果也採用科學記數法的方法表示,就得將指數統一然後提取公因數進行計算。這裏就涉及到一個 對階運算 [3] ,為了儘可能減小精度損失,需要遵守小階對大階(即將較小的指數轉換為較大的指數)的原則。在這個問題中,我們要將指數統一成-3。因此,0.1在經過對階操作後的二進制,是這樣的:

0 01111111100 (0.)1100110011001100110011001100110011001100110011001101

尾數需要向右移一位,右移超出的部分進行 0舍1入 運算。默認省略的整數部分的 1 被移到小數部分了,因此整數部分變成了0。

二進制加法運算

舍入運算

這個結果有兩個問題:

  1. 不符合科學記數法的規則。

  2. 尾數部分存在超出位數的情況。

因此要對結果做出調整,首先將結果變為“1.”開頭的,即小數點向左移一位,變成:

1.00110011001100110011001100110011001100110011001100111

同時,要將指數加1:變成:

01111111101

最後,依然根據0舍1入的原則,將尾數部分超出52位以外的部分做舍入運算,結果為:

1.0011001100110011001100110011001100110011001100110100

因此,最終的完整結果為:

0 01111111101 (1.)0011001100110011001100110011001100110011001100110100

最高位為 1,得到的二進制數如下所示:

2^-2 * 1.0011001100110011001100110011001100110011001100110100

二進制轉十進制

轉換為十進制即為:

0.30000000000000004

做舍入操作,無可避免的會引起精度丟失

四捨五入函數如何避免這個問題

由於四捨五入在統計數據時十分常見,所以你可能需要這樣一個函數,來實現完美的四捨五入

主要做了以下操作:

  1. 把所有數字轉換成一個 number[];

  2. 從最後一個數字開始計算是否 > 4;

  3. 如果 <= 4 則 break;

  4. 如果 > 4 則往遍歷一位,+1;

  5. 再判斷 +1 後的值是否 === 10

  6. 如果不是 10 則 break;

  7. 如果當前值是 10 ,則變成 0,並記錄下是否是在第一位,即:在最前方補1;

  8. 再接着for循環,i--;然後 +1,直到打破循環;

// ...

// 核心代碼
// 匹配出所有的數字 是個 int[] 1.223 [1,2,2,3]
const numArr = zeroStrNum.match(/\d/g) || [];
// 從最後一位數字是否大於4算起
if (parseInt(numArr[numArr.length - 1], 10) > 4) {
// 如果最後一位大於4,則往前遍歷+1
for (let i = numArr.length - 2; i >= 0; i--) {
numArr[i] = String(parseInt(numArr[i], 10) + 1);
// 判斷這位數字 +1 後會不會是 10
if (numArr[i] === '10') {
// 10的話處理一下變成 0,再次for循環,相當於給前面一個 +1
numArr[i] = '0';
// 是否是進位到最前面,zeroStrNum在開頭補的0了
flag = i !== 1;
} else {
// 小於10的話,就打斷循環,進位成功
break;
}
}
}

// ...

參考文獻:

  1. http://zhuanlan.zhihu.com/p/103254614

  2. http://zhuanlan.zhihu.com/p/363254961

  3. http://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Number/toFixed

  4. http://www.boatsky.com/blog/26

參考資料

[1]

Number.prototype.toFixed(): http://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Number/toFixed

[2]

可以在這直接查看結果: http://tool.oschina.net/hexconvert

[3]

對階運算: http://www.cnblogs.com/yilang/p/11277201.html

- END -

:heart: 謝謝支持

以上便是本次分享的全部內容,希望對你有所幫助^_^

喜歡的話別忘了 分享、點贊、收藏 三連哦~。

歡迎關注公眾號 ELab團隊 收貨大廠一手好文章~

我們來自字節跳動,是旗下大力教育前端部門,負責字節跳動教育全線產品前端開發工作。

我們圍繞產品品質提升、開發效率、創意與前沿技術等方向沉澱與傳播專業知識及案例,為業界貢獻經驗價值。包括但不限於性能監控、組件庫、多端技術、Serverless、可視化搭建、音視頻、人工智能、產品設計與營銷等內容。

歡迎感興趣的同學在評論區或使用內推碼內推到作者部門拍磚哦

字節跳動校/社招投遞鏈接: http://job.toutia o.com/

內推碼: 7 EZKXME