<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          后端問(wèn)為什么前端數(shù)值精度會(huì)丟失?

          共 11445字,需瀏覽 23分鐘

           ·

          2023-09-08 06:47

          前言

          相信各位前端小伙伴在日常工作中不免會(huì)涉及到使用 JavaScript 處理 數(shù)值 相關(guān)的操作,例如 數(shù)值計(jì)算保留指定小數(shù)位接口返回?cái)?shù)值過(guò)大 等等,這些操作都有可能導(dǎo)致原本正常的數(shù)值在 JavaScript 中確表現(xiàn)得異常(精度丟失),這也是被很多開(kāi)發(fā)者詬病的一點(diǎn)(你該不會(huì)還沒(méi)踩過(guò)坑吧!),當(dāng)然包括很多 后端開(kāi)發(fā)者不止一次的被問(wèn)到這個(gè)問(wèn)題)。

          本文主要包含 精度丟失場(chǎng)景、精度丟失原因、解決方案 等方面的內(nèi)容,文中若有不正確的地方歡迎在評(píng)論區(qū)分享你的見(jiàn)解。

          精度丟失場(chǎng)景

          浮點(diǎn)數(shù)的計(jì)算

          數(shù)值計(jì)算在前端的應(yīng)用還不算少,但涉及 浮點(diǎn)數(shù) 參與計(jì)算時(shí)可能會(huì)出現(xiàn)精度丟失,如下:

          加( + )

          • 正常計(jì)算:0.1 + 0.2 = 0.3

          • JavaScript 計(jì)算:0.1 + 0.2 = 0.30000000000000004

          減( - )

          • 正常計(jì)算:1 - 0.9 = 0.1

          • JavaScript 計(jì)算:1 - 0.9 = 0.09999999999999998

          乘( * )

          • 正常計(jì)算:0.0532 * 100 = 5.32

          • JavaScript 計(jì)算:0.0532 * 100 = 5.319999999999999

          除( / )

          • 正常計(jì)算:0.3 / 6 = 0.05

          • JavaScript 計(jì)算:0.3 / 6 = 0.049999999999999996

          超過(guò)最值

          所謂 超過(guò)最值(最大、最小值 指的是超過(guò)了 Number.MIN_SAFE_INTEGER(- 9007199254740991),即

          +(2^53 – 1) 或 Number.MAX_SAFE_INTEGER(+ 9007199254740991),即 -(2^53 – 1) 范圍的值,項(xiàng)目中最常見(jiàn)的就是如下幾種情況:

          • 后端返回的數(shù)值超過(guò)最值

            • 例一,后端返回的列表數(shù)據(jù),通常都會(huì)有相應(yīng)的 ID 來(lái)標(biāo)識(shí)唯一性,但后端字啊生成這個(gè) ID 時(shí)是 Long 類(lèi)型,那么該值很可能就會(huì)超過(guò) JavaScript 中能表示的最大正整數(shù),此時(shí)就導(dǎo)致精度丟失,即前端實(shí)際獲取到的 ID 值和后端返回的將不一致

            • 例二,后端可能需要將一些值通過(guò)計(jì)算之后,把對(duì)應(yīng)的結(jié)果值返回給前端,此時(shí)若該值超過(guò)了 最值,那么也會(huì)產(chǎn)生精度丟失

          • 前端進(jìn)行數(shù)值計(jì)算時(shí),計(jì)算結(jié)果超過(guò)最值

          保留指定小數(shù)位

          除了上述對(duì)涉及浮點(diǎn)數(shù)計(jì)算、超過(guò)最值的場(chǎng)景之外,我們通常還會(huì)對(duì)數(shù)值進(jìn)行保留指定小數(shù)位的處理,而部分開(kāi)發(fā)者可能會(huì)直接使用 Number.prototype.toFixed 來(lái)實(shí)現(xiàn),但這個(gè)方法卻并不能保證我們期望的效果,例如保留小數(shù)位時(shí)需要進(jìn)行 四舍五入 時(shí)就會(huì)有問(wèn)題,如下:

          console.log(1.595.toFixed(2)) // 1.59 ——> 期望為:1.60console.log(1.585.toFixed(2)) // 1.58 ——> 期望為:1.59console.log(1.575.toFixed(2)) // 1.57 ——> 期望為:1.58console.log(1.565.toFixed(2)) // 1.56 ——> 期望為:1.57console.log(1.555.toFixed(2)) // 1.55 ——> 期望為:1.56console.log(1.545.toFixed(2)) // 1.54 ——> 期望為:1.55console.log(1.535.toFixed(2)) // 1.53 ——> 期望為:1.54console.log(1.525.toFixed(2)) // 1.52 ——> 期望為:1.53console.log(1.515.toFixed(2)) // 1.51 ——> 期望為:1.52console.log(1.505.toFixed(2)) // 1.50 ——> 期望為:1.51

          精度丟失的原因

          計(jì)算機(jī)內(nèi)部實(shí)際上只能 存儲(chǔ)/識(shí)別 二進(jìn)制,因此 文檔、圖片、數(shù)字 等都會(huì)被轉(zhuǎn)換為 二進(jìn)制,而對(duì)于數(shù)字而言,雖然我們看到的是 十進(jìn)制 的表示結(jié)果,但實(shí)際上會(huì)底層會(huì)進(jìn)行 十進(jìn)制 和 二進(jìn)制 的相互轉(zhuǎn)換,而這個(gè)轉(zhuǎn)換過(guò)程就有可能會(huì)出現(xiàn) 精度丟失,因?yàn)槭M(jìn)制轉(zhuǎn)二進(jìn)制后可能產(chǎn)生 無(wú)限循環(huán) 部分,而 實(shí)際存儲(chǔ)空間是有限的

          IEEE 754 標(biāo)準(zhǔn)

          Javascript 中的數(shù)字存儲(chǔ)使用了 IEEE 754 中規(guī)定的 雙精度浮點(diǎn)數(shù) 數(shù)據(jù)類(lèi)型,雙精度浮點(diǎn)數(shù)使用 64 位(8 字節(jié)) 來(lái)存儲(chǔ)一個(gè) 浮點(diǎn)數(shù),可以表示二進(jìn)位制的 53 位 有效數(shù)字,即 (0-52 位為 1) 111...111 = (53 位為 1,0-52 位為 0) 1000...000 - 1也就是 2^53 - 1,而這也就是 JavaScript 中 Number.MAX_SAFE_INTEGER(+ 9007199254740991) 對(duì)應(yīng)的值。

          雙精度浮點(diǎn)數(shù)的組成

          雙精度浮點(diǎn)數(shù)(double) 由如下幾部分組成:

          • sign 符號(hào)位,0 為正,1 為負(fù)

            • 占 1bit,在 63 位

          • exponent 指數(shù)部分,表示 2 的幾次方

            • 占 11bit,在 52-62 位

            • 指數(shù) 采用 偏移碼表示法,即將 指數(shù)的真實(shí)值 e 加上一個(gè) 偏移量,然后得到 階碼(即計(jì)算結(jié)果)并將其表示為 二進(jìn)制數(shù)

            • 其中 偏移量 = (2^n-1) - 1n 是 指數(shù)的位數(shù)(即 n = 11,因此偏移量為 Math.pow(2, 11-1) - 1 = 1023

            • 階碼 E = 指數(shù)真值 e + 偏移碼 (2^n-1) - 1

          • mantissa 尾數(shù)部分,表示浮點(diǎn)數(shù)的精度
            • 占 52bit,在 0-51 位

            • 尾數(shù)采用 隱式 的方式表示,即在尾數(shù)的 最高位上 總是隱含著一個(gè) 1,并且隱藏在 小數(shù) 點(diǎn)的左邊 (即 1 < 尾數(shù) < 2),因此尾數(shù)的有效位數(shù)為 53 位,而不是 52 位

          十進(jìn)制浮點(diǎn)數(shù)的存儲(chǔ)過(guò)程

          有了上面的公式,接下來(lái)我們來(lái)演示一下一個(gè)十進(jìn)制浮點(diǎn)數(shù)是如何以 雙精度浮點(diǎn)數(shù) 的形式被存儲(chǔ)到計(jì)算機(jī)中的,其大致分為如下兩步:

          • 十進(jìn)制轉(zhuǎn)二進(jìn)制

            • 分別對(duì)整數(shù)部分和小數(shù)部分的十進(jìn)制轉(zhuǎn)化為二進(jìn)制

          • 求出 sign、exponent、mantissa 的值

          下面我們通過(guò) 263.3 這個(gè)數(shù)值來(lái)演示。

          十進(jìn)制轉(zhuǎn)二進(jìn)制

          分別將 263.3 的 整數(shù)部分 263 和 小數(shù)部分 0.3 轉(zhuǎn)為對(duì)應(yīng)的 二進(jìn)制數(shù),這里你可以使用便捷的 在線轉(zhuǎn)換工具,也可選擇手動(dòng)計(jì)算:

          • 整數(shù)部分 轉(zhuǎn) 二進(jìn)制

            • 一直 除以 2 直到余數(shù)為 0 或 出現(xiàn)循環(huán),然后 從下往上 將每次的余數(shù)進(jìn)行組合即可

          • 小數(shù)部分 轉(zhuǎn) 二進(jìn)制

            • 一直 乘以 2 直到乘積為 1 或 出現(xiàn)循環(huán),然后 從上往下 將每次的乘積的 整數(shù)位 進(jìn)行組合即可

          最終得到的結(jié)果就是 263.3(10) 對(duì)應(yīng)的 二進(jìn)制 為 100000111.010011001...

          求出 sign、exponent、mantissa 的值

          • 求 sign

            • 其中 sign 為符號(hào)位,且 263.3(10) 為正數(shù),因此 sign = 0

          • 求 exponent

            • 根據(jù)公式 (-1)^S x (1. M) x 2^(E-1023) 可知,其中的 尾數(shù) 要符合 1. M 的形式,因此 100000111.010011001... 中小數(shù)點(diǎn)需要往左移動(dòng) 8位 變成 1.00000111 010011001 ...

            • 其中的 8 就是 指數(shù)真值,但在實(shí)際存儲(chǔ)時(shí)是存 階碼的二進(jìn)制,根據(jù)公式 階碼 = 指數(shù)真值(8) + 偏移量(1023),即 階碼 = 1031,所以 exponent 值就為 1031 的二進(jìn)制:10000000111

          • 求 mantissa

            • 根據(jù)上一步的 1.00000111 010011001 ... 很容易知道尾數(shù) mantissa = 00000111 010011001 ...

          最終存儲(chǔ)形式

          Number.prototype.toFixed 的舍入

          關(guān)于這個(gè)方法的舍入方式,目前最多的說(shuō)法就是 銀行家算法 ,的確在大多情況下確實(shí)能夠符合 銀行家算法 的規(guī)則,但是部分情況就并不符合其規(guī)則,因此嚴(yán)格意義上來(lái)講 Number.prototype.toFixed 并不算是使用了 銀行家算法,如果你要問(wèn)為什么,請(qǐng)看 ECMAScript? 2024 Language Specification (tc39.es),在下文都會(huì)提及。

          銀行家算法

          所謂銀行家算法用一句話概括為:

          四舍六入五考慮,五后 有數(shù) 就進(jìn)一五后 無(wú)數(shù) 看 奇偶五前 為偶當(dāng) 舍去五后 為奇要 進(jìn)一

          • 四舍 指保留位后的 數(shù)值 < 5 應(yīng) 舍去4 只是個(gè)代表值

          • 六入 指保留位后的 數(shù)值 > 5 應(yīng) 進(jìn)一6 只是個(gè)代表值

          • 若保留位后的 數(shù)值 = 5,看 5 后 是否有數(shù)

            • 若 5 后 無(wú)數(shù),則看 5 前 的數(shù)值的 奇偶 來(lái)判斷

              • 若 5 前 的數(shù)值為 偶數(shù),則 舍去

              • 若 5 前 的數(shù)值為 奇數(shù),則 進(jìn)一

            • 若 5 后 有數(shù),則 進(jìn)一

          用例子來(lái)驗(yàn)證一下:

          // 四舍(1.1341).toFixed(2) = '1.13'
          // 六入(1.1361).toFixed(2) = '1.14'
          // 五后 有數(shù) ,進(jìn)一(1.1351).toFixed(2) = '1.14'
          // 五后 無(wú)數(shù),看奇偶,五前為 3 奇數(shù),進(jìn)一 (1.1350).toFixed(2) = '1.14'
          // 五后 無(wú)數(shù),看奇偶,五前為 0 偶數(shù),舍去(1.1050).toFixed(2) = '1.10'


          看起來(lái)沒(méi)有問(wèn)題是吧!

          // 五后 有數(shù),應(yīng)進(jìn)一(1.1051).toFixed(2) = 1.11 (正確 √)(1.105).toPrecision(17) = '1.1050000000000000' // 精度
          // 五后 無(wú)數(shù),看奇偶,五前為 0 偶數(shù),應(yīng)舍去(1.105).toFixed(2) = 1.10 (正確 √)
          // 五后 無(wú)數(shù),看奇偶,五前為 2 偶數(shù),應(yīng)舍去(1.125).toFixed(2) = 1.13 (不正確 ×)1.125.toPrecision(17) = '1.1250000000000000' // 精度
          // 五后 無(wú)數(shù),看奇偶,五前為 4 偶數(shù),應(yīng)舍去(1.145).toFixed(2) = 1.15 (不正確 ×)1.145.toPrecision(17) = '1.1450000000000000' // 精度
          // 五后 無(wú)數(shù),看奇偶,五前為 6 偶數(shù),應(yīng)舍去(1.165).toFixed(2) = 1.17 (不正確 ×)1.165.toPrecision(17) = '1.1650000000000000' // 精度
          // 五后 無(wú)數(shù),看奇偶,五前為 8 偶數(shù),應(yīng)舍去(1.185).toFixed(2) = 1.19 (不正確 ×)1.185.toPrecision(17) = '1.1850000000000001' // 精度

          ECMAScript 定義的 toFixed 標(biāo)準(zhǔn)

          一眼望上去是不是覺(jué)得看不懂,那么這里就來(lái)嘗試解釋一下這個(gè)標(biāo)準(zhǔn)的內(nèi)容吧(摻雜個(gè)人理解)!

          1. 讓 x = 目標(biāo)數(shù)字,如:(1.145).toFixed(2) 中 x = 1.1245

          2. 讓 f = 參數(shù),如:(1.145).toFixed(2) 中 f = 2

          3. 若 f = undefined,即 未傳參,則將 f = 0

          4. 若 f = Infinite,即傳入了 無(wú)窮值,則拋出 RangeError 異常

          5. 若 f < 0 或 f > 100,即傳入了不在 0 - 100 之間的值,則拋出 RangeError 異常

          6. 若 x = Infinite,即想要對(duì) 非準(zhǔn)確值 保留位操作,則返回其 字符串形式

            1. 例如,Infinity.toFixed(2) = 'Infinity'NaN.toFixed(2) = 'NaN'

          7. 讓 x = 計(jì)算機(jī)所能表示的數(shù)學(xué)值 ?(x)

            1. 從 數(shù)字 或 BigInt x 到 數(shù)學(xué)值 的轉(zhuǎn)換表示為 x 的數(shù)學(xué)值,或 ?(x)

          8. 讓 返回值符號(hào) s = '',即為符號(hào)定義 初始值

          9. 若 x < 0,則將 s = '-',并將 x = -x

          10. 若 x ≥ 10^21,則 返回值 m = x 對(duì)應(yīng)的科學(xué)計(jì)數(shù)法 表示的 字符串 

          11. 若 x < 10^21,則

            a. 讓 n = 一個(gè)整數(shù),其中 n / 10^f - x 盡可能接近于 0,如果有兩個(gè)這樣的 n,選擇 較大的 n

            b. 若 n = 整數(shù) 0,則 m = "0",否則,m = 由 n 的 十進(jìn)制 表示形式的數(shù)字組成的 字符串值(按順序,不帶前導(dǎo)零)

            c. 若 指數(shù) f ≠ 0,則 k = m.length

                - 若 k ≤ f,則
                       - z = 由代碼單元 0x0030(DIGIT ZERO) 的 f+1-k 次出現(xiàn)組成的 字符串

          12.            - m = z + m

                       - k = f + 1

                - 讓 a = m 的第一個(gè) k-f 碼單元

                - 讓 b = m 的其它 f 個(gè)編碼單元

                - 將 m = a + "." + b

            12. 返回 s + m 組成的字符串


          看不懂?那就挑懂的地方看

          不多說(shuō)了,還是用 (1.125).toFixed(2) = 1.13 舉個(gè)栗子吧!

          • 根據(jù)上述規(guī)范初始 x = 1.125,f = 2,s = ''

          • 根據(jù)規(guī)范 7 可知 x = 1.125.toPrecision(53) = 1.125

          • 根據(jù)規(guī)范 11.a 提供的公式:n / 10^f - x ≈ 0 代入計(jì)算:n ≈ 112.5

            • 此時(shí)最接近 n 的 整數(shù) 有 兩個(gè) 值為 110 和 112,按標(biāo)準(zhǔn)取最大的 113

            • 在按 11.c 的規(guī)范得到 m = 1.13

          • 最終返回 s + m= 1.13


          還不會(huì),再來(lái)個(gè) (-1.105).toFixed(2) = -1.10 的栗子吧!

          • 根據(jù)上述規(guī)范初始 x = 1.105,f = 2,s = '-'

          • 根據(jù)規(guī)范 7 可知 x = (-1.105).toPrecision(53) = 1.10499... 

          • 根據(jù)規(guī)范 11.a 提供的公式:n / 10^f - x ≈ 0 代入計(jì)算:n ≈ 110.4...

            • 此時(shí)最接近 n 的 整數(shù) 只有 一個(gè) 值為 110(因?yàn)橹挥行?shù)點(diǎn)后為 5 時(shí),向上 / 向下 取整才會(huì)有兩種情況)

            • 在按 11.c 的規(guī)范得到 m = 1.10

          • 最終返回 s + m= -1.13

          如何解決前端數(shù)值的精度問(wèn)題?

          雖然知道了 精度丟失 的原因,也知道了 toFixed 舍入 的邏輯,但是實(shí)際上在進(jìn)行計(jì)算時(shí),我們還是希望按照實(shí)際看到的數(shù)值來(lái)進(jìn)行計(jì)算或舍入,而不是底層轉(zhuǎn)換過(guò)的值。

          使用第三方庫(kù)

          需要的自行查閱:

          • math.js

          • big.js

          • bignumber.js

          • decimal.js

          思路擴(kuò)展

          浮點(diǎn)數(shù)計(jì)算

          浮點(diǎn)數(shù)在 JavaScript 中經(jīng)底層轉(zhuǎn)換后可能會(huì)有精度丟失,但是 安全范圍內(nèi)的整數(shù) 卻不會(huì)丟失,那么我們就可以先將 浮點(diǎn)數(shù) 轉(zhuǎn)成 整數(shù) 進(jìn)行計(jì)算后,再將計(jì)算結(jié)果成為浮點(diǎn)數(shù)。

          以 0.1 + 0.2 = 0.30000000000000004 舉個(gè)例子,如下:

          • 原式:0.1 + 0.2 = x

          • 擴(kuò)大 10 倍:0.1 * 10 + 0.2 * 10 = 10 * x

          • 變式:10 * x = 3

          • 結(jié)果:x = 0.3

          超過(guò)最值

          前面提到的 后端返回 或 前端計(jì)算 產(chǎn)生的超過(guò) 安全范圍的值,我們可以使用 BigInt 來(lái)處理,這是新增的原始值類(lèi)型,它提供了一種方法來(lái)表示 大于 2^53 - 1 的整數(shù)

          保留指定小數(shù)位

          既然 Number.prototype.toFixed() 的舍入方法并不是我們需要的,那么我們可以直接將其重寫(xiě)成符合的即可,例如:

          Number.prototype.toFixed=function (d) {     var s=this+"";     if(!d)d=0;     if(s.indexOf(".")==-1)s+=".";     s+=new Array(d+1).join("0");     if(new RegExp("^(-|\\+)?(\\d+(\\.\\d{0,"+(d+1)+"})?)\\d*$").test(s)){        var s="0"+RegExp.$2,pm=RegExp.$1,a=RegExp.$3.length,b=true;        if(a==d+2){            a=s.match(/\d/g);             if(parseInt(a[a.length-1])>4){                for(var i=a.length-2;i>=0;i--){                    a[i]=parseInt(a[i])+1;                    if(a[i]==10){                        a[i]=0;                        b=i!=1;                    }else break;                }            }            s=a.join("").replace(new RegExp("(\\d+)(\\d{"+d+"})\\d$"),"$1.$2");         }if(b)s=s.substr(1);         return (pm+s).replace(/\.$/,"");    }return this+""; }

          最后

          以上就是本文的全部?jī)?nèi)容了,由于涉及到部分內(nèi)容 計(jì)網(wǎng) 相關(guān)內(nèi)容,所以可能理解起來(lái)會(huì)比較吃力,但是跨過(guò)這道坎也就沒(méi)那么難理解了。

          希望本文對(duì)你有所幫助!!!

          端 社群



          下方加 Nealyang 好友回復(fù)「 加群」即可。



          如果你覺(jué)得這篇內(nèi)容對(duì)你有幫助,我想請(qǐng)你幫我2個(gè)小忙:

          1. 點(diǎn)個(gè)「在看」,讓更多人也能看到這篇文章


          瀏覽 978
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評(píng)論
          圖片
          表情
          推薦
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  欧美69视频 | 久久噜噜噜精品国产亚洲综合 | 亚洲天堂男人天堂 | 青娱乐最新官网 | 青青草,新红楼丁香在线 |