日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問(wèn) 生活随笔!

生活随笔

當(dāng)前位置: 首頁(yè) > 编程资源 > 编程问答 >内容正文

编程问答

为什么(2.55).toFixed(1)等于2.5?

發(fā)布時(shí)間:2025/4/16 编程问答 33 豆豆
生活随笔 收集整理的這篇文章主要介紹了 为什么(2.55).toFixed(1)等于2.5? 小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,幫大家做個(gè)參考.

上次遇到了一個(gè)奇怪的問(wèn)題:JS的(2.55).toFixed(1)輸出是2.5,而不是四舍五入的2.6,這是為什么呢?

進(jìn)一步觀察:


發(fā)現(xiàn),并不是所有的都不正常,1.55的四舍五入還是對(duì)的,為什么2.55、3.45就不對(duì)呢?

這個(gè)需要我們?cè)谠创a里面找答案。

數(shù)字在V8里面的存儲(chǔ)有兩種類型,一種是小整數(shù)用Smi,另一種是除了小整數(shù)外的所有數(shù),用HeapNumber,Smi是直接放在棧上的,而HeapNumber是需要new申請(qǐng)內(nèi)存的,放在堆里面。我們可以簡(jiǎn)單地畫一下堆和棧在內(nèi)存的位置:


如下代碼:

let obj = {};復(fù)制代碼

這里定義了一個(gè)obj的變量,obj是一個(gè)指針,它是一個(gè)局部變量,是放在棧里面的。而大括號(hào){}實(shí)例化了一個(gè)Object,這個(gè)Object需要占用的空間是在堆里申請(qǐng)的內(nèi)存,obj指向了這個(gè)內(nèi)存所在的位置。

棧和堆相比,棧的讀取效率要比堆的高,因?yàn)闂@镒兞靠梢酝ㄟ^(guò)內(nèi)存偏差得到變量的位置,如用函數(shù)入口地址減掉一個(gè)變量占用的空間(向低地址增長(zhǎng)),就能得到那個(gè)變量在內(nèi)存的內(nèi)置,而堆需要通過(guò)指針尋址,所以堆要比棧慢(不過(guò)棧的可用空間要比堆小很多)。因此局部變量如指針、數(shù)字等占用空間較小的,通常是保存在棧里的。

對(duì)于以下代碼:

let smi = 1;復(fù)制代碼

smi是一個(gè)Number類型的數(shù)字。如果這種簡(jiǎn)單的數(shù)字也要放在堆里面,然后搞個(gè)指針指向它,那么是劃不來(lái)的,無(wú)論是在存儲(chǔ)空間或者讀取效率上。所以V8搞了一個(gè)叫Smi的類,這個(gè)類是不會(huì)被實(shí)例化的,它的指針地址就是它存儲(chǔ)的數(shù)字的值,而不是指向堆空間。因?yàn)橹羔槺旧砭褪且粋€(gè)整數(shù),所以可以把它當(dāng)成一個(gè)整數(shù)用,反過(guò)來(lái),這個(gè)整數(shù)可以類型轉(zhuǎn)化為Smi的實(shí)例指針,就可以調(diào)Smi類定義的函數(shù)了,如獲取實(shí)際的整數(shù)值是多少。

如下源碼的注釋:

// Smi represents integer Numbers that can be stored in 31 bits. // Smis are immediate which means they are NOT allocated in the heap. // The this pointer has the following format: [31 bit signed int] 0 // For long smis it has the following format: // [32 bit signed int] [31 bits zero padding] 0 // Smi stands for small integer.復(fù)制代碼

在一般系統(tǒng)上int為32位,使用前面的31位表示整數(shù)的值(包括正負(fù)符號(hào)),而如果是64位的話,使用前32位表示整數(shù)的值。所以32位的時(shí)候有31位來(lái)表示數(shù)據(jù),再減去一個(gè)符號(hào)位,還剩30位,所以Smi最大整數(shù)為:

2 ^ 30 - 1 = 1073741823 = 10億

大概為10億。

到這里你可能會(huì)有一個(gè)問(wèn)題,為什么要搞這么麻煩,不直接用基礎(chǔ)類型如int整型來(lái)存就好了,還要搞一個(gè)Smi的類呢?這可能是因?yàn)閂8里面對(duì)JS數(shù)據(jù)的表示都是繼承于根類Object的(注意這里的Object不是JS的Object,JS的Object對(duì)應(yīng)的是V8的JSObject),這樣可以做一些通用的處理。所以小整數(shù)也要搞一個(gè)類,但是又不能實(shí)例化,所以就用了這樣的方法——使用指針存儲(chǔ)值。

大于21億和小數(shù)是使用HeapNumber存儲(chǔ)的,和JSObject一樣,數(shù)據(jù)是存在堆里面的,HeapNumber存儲(chǔ)的內(nèi)容是一個(gè)雙精度浮點(diǎn)數(shù),即8個(gè)字節(jié) = 2 words = 64位。關(guān)于雙精度浮點(diǎn)數(shù)的存儲(chǔ)結(jié)構(gòu)我已經(jīng)在《為什么0.1 + 0.2不等于0.3?》做了很詳細(xì)的介紹。這里可以再簡(jiǎn)單地提一下,如源碼的定義:

static const int kMantissaBits = 52;static const int kExponentBits = 11;復(fù)制代碼

64位里面,尾數(shù)占了52位,而指數(shù)用了11位,還有一位是符號(hào)位。當(dāng)這個(gè)雙精度的空間用于表示整數(shù)的時(shí)候,是用的52位尾數(shù)的空間,因?yàn)檎麛?shù)是能夠用二進(jìn)制精確表示的,所以52位尾數(shù)再加上隱藏的整數(shù)位的1(這個(gè)1是怎么來(lái)的可參考上一篇)能表示的最大值為2 ^ 53 - 1:

// ES6 section 20.1.2.6 Number.MAX_SAFE_INTEGER const double kMaxSafeInteger = 9007199254740991.0; // 2^53-1復(fù)制代碼

這是一個(gè)16位的整數(shù),進(jìn)而可以知道雙精度浮點(diǎn)數(shù)的精確位數(shù)是15位,并且有90%的概率可以認(rèn)為第16位是準(zhǔn)確的。


這樣我們就知道了,數(shù)在V8里面是怎么存儲(chǔ)的。對(duì)于2.55使用的是雙精度浮點(diǎn)數(shù),把2.55的64位存儲(chǔ)打印出來(lái)是這樣的:

對(duì)于(2.55).toFixed(1),源碼里面是這么進(jìn)行的,首先把整數(shù)位2取出來(lái),轉(zhuǎn)成字符串,然后再把小數(shù)位取出來(lái),根據(jù)參數(shù)指定的位數(shù)進(jìn)行舍入,中間再拼個(gè)小數(shù)點(diǎn),就得到了四舍五入的字符串結(jié)果。

整數(shù)部分怎么取呢?2.55的的尾數(shù)部分(加上隱藏的1)為數(shù)a:

1.01000110011...

它的指數(shù)位是1,所以把這個(gè)數(shù)左移一位就得到數(shù)b:

10.1000110011...

a原本是52位,左移1位就變成了53位的數(shù),再把b右移52 - 1 = 51位就得到整數(shù)部分為二進(jìn)制的10即十進(jìn)制的2。再用b減掉10左移51位的值,就得到了小數(shù)部分。這個(gè)實(shí)際的計(jì)算過(guò)程是這樣的:

// 尾數(shù)右移51位得到整數(shù)部分 uint64_t integrals = significand >> -exponent; // exponent = 1 - 52 // 尾數(shù)減掉整數(shù)部分得到小數(shù)部分 uint64_t fractionals = significand - (integrals << -exponent);復(fù)制代碼

接下來(lái)的問(wèn)題——整數(shù)怎么轉(zhuǎn)成字符串呢?源代碼如下所示:

static void FillDigits32(uint32_t number, Vector<char> buffer, int* length) {int number_length = 0;// We fill the digits in reverse order and exchange them afterwards.while (number != 0) {char digit = number % 10;number /= 10;buffer[(*length) + number_length] = '0' + digit;number_length++;}// Exchange the digits.int i = *length;int j = *length + number_length - 1;while (i < j) {char tmp = buffer[i];buffer[i] = buffer[j];buffer[j] = tmp;i++;j--;}*length += number_length; }復(fù)制代碼

就是把這個(gè)數(shù)不斷地模以10,就得到個(gè)位數(shù)digit,digit加上數(shù)字0的ascii編碼就得到個(gè)位數(shù)的ascii碼,它是一個(gè)char型的。在C/C++/Java/Mysql里面char是使用單引號(hào)表示的一種變量,用一個(gè)字節(jié)表示ascii符號(hào),存儲(chǔ)的實(shí)際值是它的ascii編碼,所以可以和整數(shù)相互轉(zhuǎn)換,如'0' + 1就得到'1'。每得到一個(gè)個(gè)位數(shù),就除以10,相當(dāng)十進(jìn)制里面右移一位,然后繼續(xù)處理下一個(gè)個(gè)位數(shù),不斷地把它放到char數(shù)組里面(注意C++里面的整型相除是會(huì)把小數(shù)舍去的,不會(huì)像JS那樣)。

最后再把這個(gè)數(shù)組反轉(zhuǎn)一下,因?yàn)樯厦嫣幚砗?#xff0c;個(gè)位數(shù)跑到前面去了。


小數(shù)部分是怎么轉(zhuǎn)的呢?如下代碼所示:

int point = -exponent; // exponent = -51 // fractional_count表示需要保留的小數(shù)位,toFixed(1)的話就為1 for (int i = 0; i < fractional_count; ++i) {if (fractionals == 0)break;fractionals *= 5; // fractionals = fractionals * 10 / 2;point--;char digit = static_cast<char>(fractionals >> point);buffer[*length] = '0' + digit;(*length)++;fractionals -= static_cast<uint64_t>(digit) << point; } // If the first bit after the point is set we have to round up. if (((fractionals >> (point - 1)) & 1) == 1) {RoundUp(buffer, length, decimal_point); }復(fù)制代碼

如果是toFixed(n)的話,那么會(huì)先把前n位小數(shù)轉(zhuǎn)成字符串,然后再看n + 1位的值是需要進(jìn)一位。

在把前n位小數(shù)轉(zhuǎn)成字符串的時(shí)候,是先把小數(shù)位乘以10,然后再右移50 + 1 = 51位,就得到第1位小數(shù)(代碼里面是乘以5,主要是為了避免溢出)。小數(shù)位乘以10之后,第1位小數(shù)就跑到整數(shù)位了,然后再右移原本的尾數(shù)的51位就把小數(shù)位給丟掉了,因?yàn)槭O碌?1位肯定是小數(shù)部分了,所以就得到了第一位小數(shù)。然后再減掉整數(shù)部分就得到去掉1位小數(shù)后剩下的小數(shù)部分,由于這里只循環(huán)了一次所以就跳出循環(huán)了。

接著判斷是否需要四舍五入,它判斷的條件是剩下的尾數(shù)的第1位是否為1,如果是的話就進(jìn)1,否則就不處理。上面減掉第1位小數(shù)后還剩下0.05:

實(shí)際上存儲(chǔ)的值并不是0.05,而是比0.05要小一點(diǎn):

由于2.55不是精確表示的,而2.5是可以精確表示的,所以2.55 - 2.5就可以得到0.05存儲(chǔ)的值。可以看到確實(shí)是比0.05小。

按照源碼的判斷,如果剩下的尾數(shù)第1位不是1就不進(jìn)位,由于剩下的尾數(shù)第1位是0,所以不進(jìn)位,因此就導(dǎo)致了(2.55).toFixed(1)輸入結(jié)果是2.5.

根本原因在于2.55的存儲(chǔ)要比實(shí)際存儲(chǔ)小一點(diǎn),導(dǎo)致0.05的第1位尾數(shù)不是1,所以就被舍掉了。


那怎么辦呢?難道不能用toFixed了么?

知道原因后,我們可以做一個(gè)修正:

if (!Number.prototype._toFixed) {Number.prototype._toFixed = Number.prototype.toFixed; } Number.prototype.toFixed = function(n) {return (this + 1e-14)._toFixed(n); };復(fù)制代碼

就是把toFixed加一個(gè)很小的小數(shù),這個(gè)小數(shù)經(jīng)實(shí)驗(yàn),1e-14就行了。這個(gè)可能會(huì)造成什么影響呢,會(huì)不會(huì)導(dǎo)致原本不該進(jìn)位的進(jìn)位了?加上一個(gè)14位的小數(shù)可能會(huì)導(dǎo)致13位進(jìn)1。但是如果兩個(gè)數(shù)相差1e-14的話,其實(shí)幾乎可以認(rèn)為這兩個(gè)數(shù)是相等的,所以加上這個(gè)造成的影響幾乎是可以忽略不計(jì)的,除非你要求的精度特別高。這個(gè)數(shù)和Number.EPSILON就差了一點(diǎn)點(diǎn):

這樣處理之后,toFixed就正常了:


本文通過(guò)V8源碼,解釋了數(shù)在內(nèi)存里面是怎么存儲(chǔ)的,并且對(duì)內(nèi)存棧、堆存儲(chǔ)做了一個(gè)普及,討論了源碼里面toFixed是怎么進(jìn)行的,導(dǎo)致沒(méi)有進(jìn)位的原因是什么,怎么做一個(gè)修正。


總結(jié)

以上是生活随笔為你收集整理的为什么(2.55).toFixed(1)等于2.5?的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。

如果覺得生活随笔網(wǎng)站內(nèi)容還不錯(cuò),歡迎將生活随笔推薦給好友。