你可能需要一個四舍五入的工具函數(shù)
大廠技術 堅持周更 精選好文
目前存在什么問題
問題:toFixed函數(shù)可以滿足一部分小數(shù)的四舍五入,首先可以看下mdn對于 Number.prototype.toFixed()[1] 的定義。
mdn的例子也有這個例子
2.55.toFixed(1) // 返回 '2.5'. Note it rounds down - see warning above
警告:浮點數(shù)不能精確地用二進制表示所有小數(shù)。這可能會導致意外的結果,例如
0.1 + 0.2 === 0.3返回false.
mdn的說法是 浮點數(shù)的小數(shù)計算會出現(xiàn)異常。因此toFixed函數(shù)并不能滿足嚴格意義上的四舍五入。
為什么不使用下面的方法進行四舍五入
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;
其實這個可以滿足大部分場景,但仍然有兩個小問題:
如果num本身沒超過 Number.MAX_SAFE_INTEGER 但是 乘以 rate 以后超過了,則可能又會發(fā)生一些意料之外的case 對于某些場景還是無法處理,如 1.255 保留兩位小數(shù),主要原因是也發(fā)生了精度丟失。


0.1 + 0.2 !== 0.3 的具體原因
JavaScript 中所有數(shù)字包括整數(shù)和小數(shù)都只有一種類型 — Number。它的實現(xiàn)遵循 IEEE 754 標準,使用 64 位固定長度來表示,也就是標準的 double 雙精度浮點數(shù)。
整個計算過程要經(jīng)歷以下幾個步驟:
十進制轉(zhuǎn)二進制
先把0.1轉(zhuǎn)換為二進制,見下圖:

這個處理過程是一個無限循環(huán)的狀態(tài),可以在這直接查看結果[2],最后結果是0.0001100110011001100110011001100110011001100110011001101...
0011 將會無限循環(huán)
二進制轉(zhuǎn)科學記數(shù)法
1.1(0011)… * 2^-4(小數(shù)點向右移4位,二進制中底數(shù)為2)
對科學記數(shù)法數(shù)據(jù)的二進制表示

64位存儲科學記數(shù)法
第一位是符號位,0是正數(shù),1是負數(shù),(-1的0次方還是1次方),case里就是 0
其后的11位(指數(shù)部分)用于存儲科學記數(shù)法中指數(shù)的二進制數(shù),11位的存儲范圍是 Math.pow(2, 11), 即2048,其中以1023作為正負分界線,這個case里,-4 就是 1023-4 = 1019,轉(zhuǎn)換成二進制后為:01111111011
剩余的52位(尾數(shù)部分)用于存儲科學記數(shù)法中尾數(shù)小數(shù)點后52位
所以0.1的二進制是
0 01111111011 1001100110011001100110011001100110011001100110011010
同理0.2的二進制是
0 01111111100 1001100110011001100110011001100110011001100110011010
對階運算
0.1的指數(shù)是-4,0.2的指數(shù)是-3。要想將他們運算的結果也采用科學記數(shù)法的方法表示,就得將指數(shù)統(tǒng)一然后提取公因數(shù)進行計算。這里就涉及到一個對階運算[3],為了盡可能減小精度損失,需要遵守小階對大階(即將較小的指數(shù)轉(zhuǎn)換為較大的指數(shù))的原則。在這個問題中,我們要將指數(shù)統(tǒng)一成-3。因此,0.1在經(jīng)過對階操作后的二進制,是這樣的:
0 01111111100 (0.)1100110011001100110011001100110011001100110011001101

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

舍入運算
這個結果有兩個問題:
不符合科學記數(shù)法的規(guī)則。 尾數(shù)部分存在超出位數(shù)的情況。
因此要對結果做出調(diào)整,首先將結果變?yōu)椤?.”開頭的,即小數(shù)點向左移一位,變成:
1.00110011001100110011001100110011001100110011001100111
同時,要將指數(shù)加1:變成:
01111111101
最后,依然根據(jù)0舍1入的原則,將尾數(shù)部分超出52位以外的部分做舍入運算,結果為:
1.0011001100110011001100110011001100110011001100110100
因此,最終的完整結果為:
0 01111111101 (1.)0011001100110011001100110011001100110011001100110100
最高位為 1,得到的二進制數(shù)如下所示:
2^-2 * 1.0011001100110011001100110011001100110011001100110100
二進制轉(zhuǎn)十進制
轉(zhuǎn)換為十進制即為:
0.30000000000000004
做舍入操作,無可避免的會引起精度丟失
四舍五入函數(shù)如何避免這個問題
由于四舍五入在統(tǒng)計數(shù)據(jù)時十分常見,所以你可能需要這樣一個函數(shù),來實現(xiàn)完美的四舍五入
主要做了以下操作:
把所有數(shù)字轉(zhuǎn)換成一個 number[]; 從最后一個數(shù)字開始計算是否 > 4; 如果 <= 4 則 break; 如果 > 4 則往遍歷一位,+1; 再判斷 +1 后的值是否 === 10 如果不是 10 則 break; 如果當前值是 10 ,則變成 0,并記錄下是否是在第一位,即:在最前方補1; 再接著for循環(huán),i--;然后 +1,直到打破循環(huán);
// ...
// 核心代碼
// 匹配出所有的數(shù)字 是個 int[] 1.223 [1,2,2,3]
const numArr = zeroStrNum.match(/\d/g) || [];
// 從最后一位數(shù)字是否大于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);
// 判斷這位數(shù)字 +1 后會不會是 10
if (numArr[i] === '10') {
// 10的話處理一下變成 0,再次for循環(huán),相當于給前面一個 +1
numArr[i] = '0';
// 是否是進位到最前面,zeroStrNum在開頭補的0了
flag = i !== 1;
} else {
// 小于10的話,就打斷循環(huán),進位成功
break;
}
}
}
// ...
參考文獻:
https://zhuanlan.zhihu.com/p/103254614 https://zhuanlan.zhihu.com/p/363254961 https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Number/toFixed https://www.boatsky.com/blog/26
參考資料
Number.prototype.toFixed(): https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Number/toFixed
[2]可以在這直接查看結果: https://tool.oschina.net/hexconvert
[3]對階運算: https://www.cnblogs.com/yilang/p/11277201.html
?? 謝謝支持
以上便是本次分享的全部內(nèi)容,希望對你有所幫助^_^
喜歡的話別忘了 分享、點贊、收藏 三連哦~。
歡迎關注公眾號 趣談前端 收貨大廠一手好文章~

點個在看你最好看
