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

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

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

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

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

Math.round
理論上講,一個(gè)兩位小數(shù)乘 100 后一定會(huì)得到一個(gè)整數(shù),一個(gè)三位小數(shù)乘 1000 以后一定也會(huì)得到一個(gè)整數(shù)。同理可知,一個(gè) n 位小數(shù)乘(10^n)后,一定可以得到一個(gè)整數(shù)!
雖然在計(jì)算機(jī)世界中小數(shù)計(jì)算有些誤差,但通過上述代碼我們知道,這個(gè)誤差小到幾乎可以忽略,那么我們是不是可以大膽一點(diǎn),放大之后無需比較,直接四舍五入!
我們修改以上代碼,舍棄compare函數(shù)。
代碼如下:
const getPower = (a, b, c) => {
// 獲取a,b小數(shù)位長度,如沒有小數(shù)位則默認(rèn)值為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);
// 計(jì)算返回放大倍數(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)
在瀏覽器中運(yùn)行后發(fā)現(xiàn),結(jié)果依然正確。
如下圖所示:

封裝完善
基于以上推導(dǎo),我們可以封裝一個(gè)簡易的計(jì)算函數(shù)。
代碼如下:
function compute(type, ...args) {
// 計(jì)算放大倍數(shù)
const getPower = (numbers) => {
const lens = numbers.map(num => num.toString().split(".")[1]?.length || 0);
// 獲取最大長度
const len = Math.max(...lens);
// 計(jì)算返回放大倍數(shù)
return Math.pow(10, len)
}
// 獲取放大倍數(shù)
const power = getPower(args);
// 獲取放大后的值
const newNumbers = args.map(num => Math.round(num * power));
// 計(jì)算結(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);
}
}
}
// 驗(yàn)證
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ù)已實(shí)現(xiàn)基本的四則運(yùn)算,且可以鏈?zhǔn)秸{(diào)用。
結(jié)語
若有錯(cuò)誤,請(qǐng)務(wù)必給予指正。 謝謝!
以上就是JS中數(shù)學(xué)計(jì)算精度問題的解決方案的詳細(xì)內(nèi)容,更多關(guān)于JS數(shù)學(xué)計(jì)算精度問題的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
javascript實(shí)現(xiàn)表單驗(yàn)證
這篇文章主要介紹了javascript實(shí)現(xiàn)表單驗(yàn)證的相關(guān)資料,以一個(gè)完整的實(shí)例對(duì)javascript實(shí)現(xiàn)表單驗(yàn)證的方法進(jìn)行分析,感興趣的小伙伴們可以參考一下2016-01-01
微信小程序?qū)崿F(xiàn)星級(jí)評(píng)價(jià)效果
這篇文章主要為大家詳細(xì)介紹了微信小程序?qū)崿F(xiàn)星級(jí)評(píng)價(jià)效果,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2018-12-12
layui radio點(diǎn)擊事件實(shí)現(xiàn)input顯示和隱藏的例子
今天小編就為大家分享一篇layui radio點(diǎn)擊事件實(shí)現(xiàn)input顯示和隱藏的例子,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過來看看吧2019-09-09
d3.js實(shí)現(xiàn)簡單的網(wǎng)絡(luò)拓?fù)鋱D實(shí)例代碼
最近一直在學(xué)習(xí)d3.js,大家都知道d3.js是一個(gè)非常不錯(cuò)的數(shù)據(jù)可視化庫,我們可以用它來做一些比較酷的東西,比如可以來顯示一些簡單的網(wǎng)絡(luò)拓?fù)鋱D,這篇文中就通過實(shí)例代碼給大家介紹了如何利用d3.js實(shí)現(xiàn)簡單的網(wǎng)絡(luò)拓?fù)鋱D,有需要的朋友們可以參考借鑒,下面來一起看看吧。2016-11-11
JavaScript中call和apply方法的區(qū)別實(shí)例分析
這篇文章主要介紹了JavaScript中call和apply方法的區(qū)別,結(jié)合實(shí)例形式分析call和apply方法的功能、原理及相關(guān)使用操作區(qū)別,需要的朋友可以參考下2018-08-08
客戶端JavaScript的線程池設(shè)計(jì)詳解
這篇文章主要為大家介紹了客戶端JavaScript的線程池設(shè)計(jì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下,希望能夠給你帶來幫助2022-01-01

