你可能需要一個四捨五入的工具函數
大 廠 技 術 堅 持 周 更 精 選 好 文
目前存在什麼問題
問題: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;
其實這個可以滿足大部分場景,但仍然有兩個小問題:
-
如果num本身沒超過 Number.MAX_SAFE_INTEGER 但是 乘以 rate 以後超過了,則可能又會發生一些意料之外的case
-
對於某些場景還是無法處理,如 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.”開頭的,即小數點向左移一位,變成:
1.00110011001100110011001100110011001100110011001100111
同時,要將指數加1:變成:
01111111101
最後,依然根據0舍1入的原則,將尾數部分超出52位以外的部分做舍入運算,結果為:
1.0011001100110011001100110011001100110011001100110100
因此,最終的完整結果為:
0 01111111101 (1.)0011001100110011001100110011001100110011001100110100
最高位為 1,得到的二進制數如下所示:
2^-2 * 1.0011001100110011001100110011001100110011001100110100
二進制轉十進制
轉換為十進制即為:
0.30000000000000004
做舍入操作,無可避免的會引起精度丟失
四捨五入函數如何避免這個問題
由於四捨五入在統計數據時十分常見,所以你可能需要這樣一個函數,來實現完美的四捨五入
主要做了以下操作:
-
把所有數字轉換成一個 number[];
-
從最後一個數字開始計算是否 > 4;
-
如果 <= 4 則 break;
-
如果 > 4 則往遍歷一位,+1;
-
再判斷 +1 後的值是否 === 10
-
如果不是 10 則 break;
-
如果當前值是 10 ,則變成 0,並記錄下是否是在第一位,即:在最前方補1;
-
再接着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;
}
}
}
// ...
參考文獻:
-
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
可以在這直接查看結果: https://tool.oschina.net/hexconvert
對階運算: https://www.cnblogs.com/yilang/p/11277201.html
- END -
:heart: 謝謝支持
以上便是本次分享的全部內容,希望對你有所幫助^_^
喜歡的話別忘了 分享、點贊、收藏 三連哦~。
歡迎關注公眾號 ELab團隊 收貨大廠一手好文章~
我們來自字節跳動,是旗下大力教育前端部門,負責字節跳動教育全線產品前端開發工作。
我們圍繞產品品質提升、開發效率、創意與前沿技術等方向沉澱與傳播專業知識及案例,為業界貢獻經驗價值。包括但不限於性能監控、組件庫、多端技術、Serverless、可視化搭建、音視頻、人工智能、產品設計與營銷等內容。
歡迎感興趣的同學在評論區或使用內推碼內推到作者部門拍磚哦
字節跳動校/社招投遞鏈接: https://job.toutia o.com/
內推碼: 7 EZKXME
- 使用 WebAssembly 打造定製 JS Runtime
- 前端也要懂算法,不會算法也能微調一個 NLP 預訓練模型
- 聯機遊戲原理入門即入土 -- 入門篇
- Plasmo Framework:次世代的瀏覽器插件開發框架
- 深入理解 Mocha 測試框架:從零實現一個 Mocha
- Single Source of Truth:XCode SwiftUI 的界面編輯的設計理念
- 深入理解 D3.js 可視化庫之力導向圖原理與實現
- 淺析神經網絡 Neural Networks
- Cutter - Web視頻剪輯工具原理淺析
- 你可能需要一個四捨五入的工具函數
- 淺析eslint原理
- 最小編譯器the-super-tiny-compiler
- Git存儲原理及部分實現
- 淺談短鏈的設計
- Web組件構建庫-Lit
- 使用Svelte開發Chrome Extension
- Web3.0開發入門
- vscode插件原理淺析與實戰
- 深入淺出 Web Audio API
- 探祕HTTPS