JS中數(shù)學計算精度問題的解決方案
故事從0.1+0.2說起
0.1+0.2是否等于0.3呢?
這是一個前端人耳熟能詳?shù)墓适?,每一個初入前端世界的人,應(yīng)該都會被它來一次靈魂拷問。它的出現(xiàn),似乎打破了人們以往對于代碼世界“執(zhí)行嚴謹、一絲不茍”的刻板印象。然而,這看起來“不夠嚴謹”的形成原因,卻正是因為底層代碼執(zhí)行的足夠嚴謹!
在初入前端世界的時候,有那么一瞬,我甚至在想難道是底層對于0.1和0.2有一種特殊的感情?
然而事實并非如此,能被底層這種龐然大物看上并針對的,當然不會只有0.1和0.2這兩個看起來平平無奇的數(shù)字,而是包含了這兩個數(shù)字在內(nèi)的一批特殊存在。
如下圖所示:

當然,除上述圖片內(nèi)的數(shù)字之外,還有更多的其他數(shù)字也在這反常理的隊列之內(nèi)。
然而,這篇文章我們并不是來深入討論這些特殊的數(shù)字在進行數(shù)學計算時,與底層究竟產(chǎn)生了什么樣的恩怨糾葛。我們只需要簡單知道:在計算機世界中,所有信息最后都是以二進制存儲的,可是數(shù)字中的小數(shù)部分在按照一定規(guī)則轉(zhuǎn)換為二進制時,有些數(shù)字會產(chǎn)生無限循環(huán)的現(xiàn)象,但計算機精度位數(shù)是有限的,所以對超出位數(shù)的部分做了四舍五入的計算,因此造成了精度的丟失。
本文僅僅針對以上現(xiàn)象,結(jié)合日常開發(fā)的實踐,討論一些解決問題的方法。
初步解決
既然是因為小數(shù)部分在轉(zhuǎn)換二進制時做了四舍五入的處理,那么計算時先將小數(shù)轉(zhuǎn)為整數(shù)再計算是不是就可以了?
依據(jù)上面的思想,在javascript中進行小數(shù)計算,通常會采用放大倍數(shù)取整之后再計算,得出結(jié)果之后再縮小還原的技術(shù)方案。
例:計算0.1+0.2,通常會將0.1和0.2放大10倍,相加之后再縮小10倍
代碼如下:
/** * 已知:a為0.1,b為0.2 * 求:a與b的和 * */ const a = 0.1; const b = 0.2; const result = ((a * 10) + (b * 10)) / 10; console.log(result) // 0.3
如上,針對已知小數(shù)位數(shù)的數(shù)字,我們可以直接采用放大相應(yīng)倍數(shù)取整,然后再計算的方式來規(guī)避小數(shù)計算的精度問題。
可是在實際的業(yè)務(wù)開發(fā)中,對于需要進行計算處理的數(shù)字,我們往往無法預先獲知數(shù)字包含的小數(shù)位數(shù)。對于此種情況,我們便需要先確定小數(shù)位數(shù),然后確定放大倍數(shù),再進行計算。
代碼如下:
/**
* 已知:a,b為兩個精度隨機的小數(shù)
* 求:a與b的和
*/
// 生成精度隨機的小數(shù)
const getNumber = () => {
const len = Math.random() * 10;
const num = Number((Math.random() * 10).toFixed(len))
return num
}
// 計算放大倍數(shù)
const getPower = (a, b) => {
// 獲取a,b小數(shù)位長度,如沒有小數(shù)位則默認值為0
const aLen = a.toString().split(".")[1]?.length || 0;
const bLen = b.toString().split(".")[1]?.length || 0;
// 獲取最大長度
const len = Math.max(aLen, bLen);
// 計算返回放大倍數(shù)
return Math.pow(10, len)
}
const a = getNumber();
const b = getNumber();
const power = getPower(a, b);
const result = ((a * power) + (b * power)) / power;
因為以上代碼中,a和b皆由getNumber函數(shù)隨機生成,為了便于觀察,我們添加log后,在瀏覽器中運行代碼。
如下圖所示:

觀察可知,計算結(jié)果正確。以上,我們通過使用getPower函數(shù)確定放大的倍數(shù),然后進行計算。這也是目前大部分同學解決小數(shù)計算精度問題的主要方式。
然而,故事到這里就結(jié)束了嗎?
當然不是!
以上方式雖然解決了一些精度問題,但是并沒有解決所有的精度問題。在這個特殊的小數(shù)群體中,并不是所有的小數(shù)都可以通過放大倍數(shù)來取整的!
如下圖所示:

因此先放大再計算也并不是十分可靠,如下圖所示:

大膽取舍
Number.EPSILON
我們已經(jīng)知道,精度的誤差是由于底層在計算時做了一些四舍五入造成的,因此我們分析后可以斷定被舍棄的部分一定是小于可以表示的最小浮點數(shù)的。
例如:
有數(shù)字 a 為 1.234,對 a 做保留兩位小數(shù)的處理后,得到數(shù)字 b ,b的值為1.23。則上述操作中舍棄的部分 0.004,一定小于保留精度 0.01
基于以上分析,我們有理由相信:在 javascript 中當兩個數(shù)字之間的差值小于可以表示的最小浮點數(shù),那么我們就認為這兩個數(shù)字相等。
可是,最小的浮點數(shù)該如何獲取呢?
javascript 為我們提供了這樣一個屬性:Number.EPSILON 靜態(tài)數(shù)據(jù)屬性,表示 1 與大于 1 的最小浮點數(shù)之間的差值。
詳細介紹可查看MDN。
我們對之前的代碼做一些優(yōu)化,在放大一定倍數(shù)之后,做差值比較,確認最終結(jié)果。
代碼如下:
const getPower = (a, b, c) => {
// 獲取a,b小數(shù)位長度,如沒有小數(shù)位則默認值為0
const aLen = a.toString().split(".")[1]?.length || 0;
const bLen = b.toString().split(".")[1]?.length || 0;
const cLen = c.toString().split(".")[1]?.length || 0;
// 獲取最大長度
const len = Math.max(aLen, bLen, cLen);
// 計算返回放大倍數(shù)
return Math.pow(10, len)
}
// 差值比價
const compare = (n) => {
const result = Math.round(n);
// 如差值小于 Number.EPSILON 則認為和取整之后的數(shù)字相等
return n - result < Number.EPSILON ? result : n;
}
var a = 19.9;
var b = 4788.4;
var c = 0.01;
var power = getPower(a, b, c);
const result = (compare((a * power)) + compare((b * power)) + compare((c * power))) / power
console.log(result)
在瀏覽器運行代碼,可知計算正確。
如下圖所示:

Math.round
理論上講,一個兩位小數(shù)乘 100 后一定會得到一個整數(shù),一個三位小數(shù)乘 1000 以后一定也會得到一個整數(shù)。同理可知,一個 n 位小數(shù)乘(10^n)后,一定可以得到一個整數(shù)!
雖然在計算機世界中小數(shù)計算有些誤差,但通過上述代碼我們知道,這個誤差小到幾乎可以忽略,那么我們是不是可以大膽一點,放大之后無需比較,直接四舍五入!
我們修改以上代碼,舍棄compare函數(shù)。
代碼如下:
const getPower = (a, b, c) => {
// 獲取a,b小數(shù)位長度,如沒有小數(shù)位則默認值為0
const aLen = a.toString().split(".")[1]?.length || 0;
const bLen = b.toString().split(".")[1]?.length || 0;
const cLen = c.toString().split(".")[1]?.length || 0;
// 獲取最大長度
const len = Math.max(aLen, bLen, cLen);
// 計算返回放大倍數(shù)
return Math.pow(10, len)
}
var a = 19.9;
var b = 4788.4;
var c = 0.01;
var power = getPower(a, b, c);
const result = (Math.round((a * power)) + Math.round((b * power)) + Math.round((c * power))) / power
console.log(result)
在瀏覽器中運行后發(fā)現(xiàn),結(jié)果依然正確。
如下圖所示:

封裝完善
基于以上推導,我們可以封裝一個簡易的計算函數(shù)。
代碼如下:
function compute(type, ...args) {
// 計算放大倍數(shù)
const getPower = (numbers) => {
const lens = numbers.map(num => num.toString().split(".")[1]?.length || 0);
// 獲取最大長度
const len = Math.max(...lens);
// 計算返回放大倍數(shù)
return Math.pow(10, len)
}
// 獲取放大倍數(shù)
const power = getPower(args);
// 獲取放大后的值
const newNumbers = args.map(num => Math.round(num * power));
// 計算結(jié)果
let result = 0;
switch (type) {
case "+":
result = newNumbers.reduce((preNumber, nextNumber) => preNumber + nextNumber, result) / power;
break;
case "-":
result = newNumbers.reduce((preNumber, nextNumber) => preNumber - nextNumber) / power;
break;
case "*":
result = newNumbers.reduce((preNumber, nextNumber) => preNumber * nextNumber) / (power ** newNumbers.length);
break;
case "/":
result = newNumbers.reduce((preNumber, nextNumber) => preNumber / nextNumber);
break;
}
return {
result,
next(nextType, ...nextArgs) {
return compute(nextType, result, ...nextArgs);
}
}
}
// 驗證
const arr = [0.1, 0.2, 29.6]
const a = compute('+', ...arr);
const b = a.next('-', 4, 2, 4);
const c = b.next('*', 100);
const d = c.next('+', 2798.4);
const e = d.next('*', 100);
const f = e.next('/', 1000);
const r = compute('+', ...arr).next('-', 4, 2, 4).next('*', 100).next('+', 2798.4).next('*', 100).next('/', 1000);
console.log('a: ', a.result) // a: 29.9
console.log('b: ', b.result) // b: 19.9
console.log('c: ', c.result) // c: 1990
console.log('d: ', d.result) // d: 4788.4
console.log('e: ', e.result) // e: 478840
console.log('f: ', f.result) // f: 478.84
console.log('r: ', r.result) // f: 478.84
經(jīng)簡單測試后,可知compute函數(shù)已實現(xiàn)基本的四則運算,且可以鏈式調(diào)用。
結(jié)語
若有錯誤,請務(wù)必給予指正。 謝謝!
以上就是JS中數(shù)學計算精度問題的解決方案的詳細內(nèi)容,更多關(guān)于JS數(shù)學計算精度問題的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
layui radio點擊事件實現(xiàn)input顯示和隱藏的例子
今天小編就為大家分享一篇layui radio點擊事件實現(xiàn)input顯示和隱藏的例子,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧2019-09-09
d3.js實現(xiàn)簡單的網(wǎng)絡(luò)拓撲圖實例代碼
最近一直在學習d3.js,大家都知道d3.js是一個非常不錯的數(shù)據(jù)可視化庫,我們可以用它來做一些比較酷的東西,比如可以來顯示一些簡單的網(wǎng)絡(luò)拓撲圖,這篇文中就通過實例代碼給大家介紹了如何利用d3.js實現(xiàn)簡單的網(wǎng)絡(luò)拓撲圖,有需要的朋友們可以參考借鑒,下面來一起看看吧。2016-11-11
JavaScript中call和apply方法的區(qū)別實例分析
這篇文章主要介紹了JavaScript中call和apply方法的區(qū)別,結(jié)合實例形式分析call和apply方法的功能、原理及相關(guān)使用操作區(qū)別,需要的朋友可以參考下2018-08-08

