big.js?如何解決精度丟失問題源碼解析
前言
想必大家日常開發(fā)中經(jīng)常碰到小數(shù)相加結(jié)果不準確的坑,也都知道這是因為精度丟失導(dǎo)致的,更是知道能通過一些庫,比如 big.js、decimal.js 等解決,但是你知道它們是怎么解決的嗎?
今天我?guī)Т蠹乙徊讲椒治?big.js 部分源碼,幫助大家理解這類庫對精度丟失的處理方式。
初窺門徑 —— 打開調(diào)試窗口
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Document</title> <script src='https://cdn.jsdelivr.net/npm/big.js@6.2.1/big.min.js'></script> </head> <body> <script> new Big(0.1); </script> </body> </html>
我們可以通過 F12 或者 Ctrl+Shift+I 打開 Chrome DevTool。
在 Sources 標簽的 Page 項里,找到未混淆的代碼文件 big.js(xxx.min.js 文件一般是壓縮混淆后的代碼,可讀性不高),這個就是我們要用到的完整源碼了。
知根知底 —— Big 構(gòu)造函數(shù)做的好事
我們先來看看 Big
構(gòu)造函數(shù)做了什么事。
function _Big_() { function Big(n) { var x = this; // 可以通過函數(shù)調(diào)用的形式來創(chuàng)建 Big 對象 if (!(x instanceof Big)) return n === UNDEFINED ? _Big_() : new Big(n); // 區(qū)分是否為 Big 示例. if (n instanceof Big) { x.s = n.s; x.e = n.e; x.c = n.c.slice(); } else { // 邊界處理 if (typeof n !== 'string') { if (Big.strict === true && typeof n !== 'bigint') { throw TypeError(INVALID + 'value'); } // Minus zero? n = n === 0 && 1 / n < 0 ? '-0' : String(n); } parse(x, n); } x.constructor = Big; } ... return Big; }
可以發(fā)現(xiàn),一開始的時候,構(gòu)造函數(shù)進行了 邊界處理 以及 入?yún)z查 ,隨后通過 parse
函數(shù)處理,最后修正構(gòu)造函數(shù)的指向。
我們先將 this
對象添加進 watches 里,接著 parse
函數(shù)后的位置打個斷點然后刷新。
看看經(jīng)過 parse
函數(shù)生成的數(shù)據(jù):
其中,c
是去除首尾的 0
之后的所有數(shù)字組成的數(shù)組,e
表示用科學(xué)計數(shù)法表示 parse
函數(shù)的入?yún)?x
時 冪的值 ,s
表示正負(1 表示正數(shù),-1 表示負數(shù))。
拿 new Big(120)
舉個例子,去除首尾 0 后,c
屬性的值為 [1, 2]
;
入?yún)?x = 120
,用科學(xué)計數(shù)法表示就是 1.2 * 10²
,e
的值就是 冪 ,也就是 2
,顯然 s = 1
。同理,new Big(1.2)
對應(yīng)的值就是 c = [1, 2], e = 0(1.2 * 10º), s = 1
。
顯然,parse
函數(shù)用來處理數(shù)據(jù),為后續(xù)的運算做準備。
對細節(jié)感興趣的小伙伴可以通過 Ctrl + F 快捷搜索研究一下。
到此為止,我們已經(jīng)窺得 Big
構(gòu)造函數(shù)的全貌,接下來我們看看 plus/add
方法做了什么吧。
抽絲剝繭 —— P.plus 源碼分析
老方法,Ctrl+F 搜索 P.plus 后回車,跳轉(zhuǎn)到該方法在文件中所在的位置。
完整源碼
P.plus = P.add = function (y) { // 1. 用 x 和 Big 兩個變量分別保存 this(調(diào)用者) 和 Big 構(gòu)造函數(shù) var e, k, t, x = this, Big = x.constructor; // 2. 將入?yún)⑥D(zhuǎn)化為 Big 對象 y = new Big(y); // 3. 判斷是否符號不同,如果不同則直接調(diào)用 minus 做減法(1 + (-1)=== 1 - 1) if (x.s != y.s) { y.s = -y.s; return x.minus(y); } // 4. 分別存儲 x 和 y 各自的小數(shù)點位置以及 number 數(shù)組 var xe = x.e, xc = x.c, ye = y.e, yc = y.c; // 5. Either zero? if (!xc[0] || !yc[0]) { if (!yc[0]) { if (xc[0]) { y = new Big(x); } else { y.s = x.s; } } return y; } // 6. copy xc 數(shù)組 xc = xc.slice(); // 7. 補 0(對齊 x 和 y 的長度) // Prepend zeros to equalise exponents. // Note: reverse faster than unshifts. if (e = xe - ye) { if (e > 0) { ye = xe; t = yc; } else { e = -e; t = xc; } t.reverse(); for (; e--;) t.push(0); t.reverse(); } // 8. 如果 xc 長度大于 yc,則交換它們 // Point xc to the longer array. if (xc.length - yc.length < 0) { t = yc; yc = xc; xc = t; } e = yc.length; // 9. 相加 // Only start adding at yc.length - 1 as the further digits of xc can be left as they are. for (k = 0; e; xc[e] %= 10) k = (xc[--e] = xc[e] + yc[e] + k) / 10 | 0; // No need to check for zero, as +x + +y != 0 && -x + -y != 0 if (k) { xc.unshift(k); ++ye; } // Remove trailing zeros. for (e = xc.length; xc[--e] === 0;) xc.pop(); y.c = xc; y.e = ye; return y; };
步驟 1 —— 變量定義
步驟 1 中使用 x
和 Big
兩個變量分別保存 this
(調(diào)用者) 和 Big
構(gòu)造函數(shù)。
步驟 2 —— 處理入?yún)?/h3>
步驟 2 中將入?yún)?y
轉(zhuǎn)為 Big
實例。
步驟 3 —— 判斷符號
步驟 3 中對判斷 x
和 y
的符號是否不同,如果不同的話,會先將 y
取反,調(diào)用 minus
方法處理,因為 一個數(shù)加上一個負數(shù)相當(dāng)于減去這個負數(shù)取反(就是 1+(-1)===1-1 的道理)。
這里顯然符號相同,因此繼續(xù)走下去。
步驟 4 —— 保存屬性
步驟 4 中將 x
和 y
的數(shù)字數(shù)組和符號位置都保存起來,至于作用是啥我也不知道,我也才調(diào)試到這呢,繼續(xù)往下看。
步驟 5 —— 處理值為 0 的情況
步驟 5 的 if
看來是進不去了,我們自己分析一下吧。
if
的判斷條件是 !xc[0] || !yc[0]
,誒,這就用到了步驟 4 的變量了。xc
就是 x
的數(shù)字數(shù)組(就是 big
實例的 c
屬性),yc
同理。這里大家注意一下,經(jīng)過構(gòu)造函數(shù)調(diào)用 parse 解析之后,實際上是已經(jīng)移除首尾的 0 了,那么 !xc[0]
和 !yc[0]
怎么可能為 true
?那肯定是有段邏輯讓它變成了 0
咯,直接看源碼,果然被我揪到了。
function parse(x, n) { ... // Determine leading zeros. for (i = 0; i < nl && n.charAt(i) == '0';) ++i; if (i == nl) { // Zero. x.c = [x.e = 0]; } else { // Determine trailing zeros. for (; nl > 0 && n.charAt(--nl) == '0';); x.e = e - i - 1; x.c = []; // Convert string to array of digits without leading/trailing zeros. for (e = 0; i <= nl;) x.c[e++] = +n.charAt(i++); } ... }
很明顯的,它都給出注釋了,當(dāng)實例化時入?yún)?x
判定為 0 的時候,x.c 和 x.e 都會被置為 0 。那步驟 5 很顯然就是個 提前返回操作 ,直接返回和 0
相加的結(jié)果。
步驟 6 —— 拷貝 xc 防止數(shù)據(jù)污染
步驟 6 中拷貝了一份 xc
數(shù)組,防止數(shù)據(jù)污染。
步驟 7 —— 補 0 對齊
這段代碼的作用是 補0 ,為的是對齊 x
和 y
的長度,這樣方便后續(xù) 按位置進行運算(有沒有做過大整數(shù)加法的小伙伴?里面有個補 0 對齊的操作)。
舉個例子,比如 big(1.2).plus(120)
,那么 t = xc = [1, 2]
,執(zhí)行 reverse + push + reverse
后就是 [0, 0, 1, 2] 和 [1, 2, 0, 0]
。
步驟 8 —— 比較 xc 和 yc 的長短
步驟 8 中,將 xc
指向長度較長的數(shù)組,yc
指向較短的數(shù)組,且將較短的 yc.length
用 e
存儲起來。
步驟 9 —— plus 操作
步驟 9 就是正戲了,到這里真正開始了 plus
操作。
這里有點大整數(shù)相加的意思,為了讓大家理解,我飯都不吃肝了個動圖。
這下大家知道為什么要將 xc
指向較長的數(shù)組,而將 yc
指向較短的數(shù)組了吧,因為較短的數(shù)組前面都是 0
,實際上這些 0
都沒必要進行相加處理了。
至此,我們的 P.plus
的源碼就分析結(jié)束了。
終章
本文就到此結(jié)束了,總覽這個解析過程,我們可以發(fā)現(xiàn) big.js 的加法操作,就是 將小數(shù)全部變成整數(shù),然后進行相加 。你不是小數(shù)運算會丟失精度嗎,那我都變成整數(shù)不就好了,計算完我再變回小數(shù),更多關(guān)于big.js 解決精度丟失的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
詳談js中標準for循環(huán)與foreach(for in)的區(qū)別
下面小編就為大家?guī)硪黄斦刯s中標準for循環(huán)與foreach(for in)的區(qū)別。小編覺得挺不錯的,現(xiàn)在就分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2017-11-11javascript css styleFloat和cssFloat
在寫js操作css的過程中發(fā)現(xiàn)float屬性在IE和firefox下對應(yīng)的js腳本是不一樣的,IE下對應(yīng)得是 styleFloat,firefox,chorme,safari下對應(yīng)的是cssFloat,可用in運算符去檢測style是否包含此屬性。2010-03-03JavaScript實現(xiàn)仿新浪微博大廳和騰訊微博首頁滾動特效源碼
最近看到朋友用JavaScript實現(xiàn)仿新浪微博大廳和未登錄騰訊微博首頁滾動效果,朋友使用jquery實現(xiàn)的,在網(wǎng)上看到有用js制作的也比較好,于是把我的內(nèi)容整理分享給大家,具體詳解請看本文2015-09-09JS及JQuery對Html內(nèi)容編碼,Html轉(zhuǎn)義
本文主要介紹了JS及JQuery對Html內(nèi)容編碼,Html轉(zhuǎn)義的方法。具有很好的參考價值,下面跟著小編一起來看下吧2017-02-02JavaScript實現(xiàn)SHA-1加密算法的方法
這篇文章主要介紹了JavaScript實現(xiàn)SHA-1加密算法的方法,實例分析了使用javascript實現(xiàn)SHA-1加密算法的技巧,具有一定參考借鑒價值,需要的朋友可以參考下2015-03-03js實現(xiàn)的格式化數(shù)字和金額功能簡單示例
這篇文章主要介紹了js實現(xiàn)的格式化數(shù)字和金額功能,結(jié)合簡單實例形式分析了javascript數(shù)字字符串轉(zhuǎn)換、運算等相關(guān)操作技巧,需要的朋友可以參考下2019-07-07