參考資料
《【轉(zhuǎn)+補(bǔ)充】深入研究js中的位運(yùn)算及用法》
《【JS時(shí)間戳】獲取時(shí)間戳的最快方式探究》
由來(lái)
日常開發(fā)中一直沒遇到過位運(yùn)算導(dǎo)致精度丟失的問題坤检,直到這天,搞10位時(shí)間戳取整的時(shí)候期吓,終于被我撞上了早歇。具體是個(gè)什么場(chǎng)景呢,我們來(lái)還原下案發(fā)現(xiàn)場(chǎng):
- 首先我們獲取一下10位的時(shí)間戳
const {
performance
} = require('perf_hooks')
// 通過performance獲取當(dāng)前時(shí)間
const t = performance.timeOrigin + performance.now()
console.log(t)
console.log(t / 1000)
console.log(t / 1000 << 0)
可以看到輸出的結(jié)果為:
1597113682985.075
1597113682.958075
1597113682
得到的t
是一個(gè)精確到微秒的時(shí)間戳讨勤。但是請(qǐng)求接口的時(shí)候需要的是一個(gè)10位(精確到秒)的時(shí)間戳箭跳,所以這里需要將它轉(zhuǎn)換為10位,自然就是?1000
即可潭千,然后通過位運(yùn)算來(lái)實(shí)現(xiàn)類似Math.trunc
的取證效果谱姓,得到了我們要的10位時(shí)間戳。至此完美解決刨晴!那問題又是如何發(fā)生的呢屉来?
- 然后獲取一下13位時(shí)間戳
按照上面的運(yùn)算規(guī)律,如果我們要獲取13位時(shí)間戳狈癞,是不是直接對(duì)t>>0
就可以了呢茄靠?我們來(lái)看一下:
const {
performance
} = require('perf_hooks')
// 通過performance獲取當(dāng)前時(shí)間
const t = performance.timeOrigin + performance.now()
console.log(t>>0)
輸出結(jié)果如下:
-614151127
WTF!!!看到了咩!5啊慨绳!居然輸出了一個(gè)負(fù)數(shù)!U媸脐雪!我們想要的結(jié)果應(yīng)該是1597113682985
才對(duì)啊恢共!為什么會(huì)出現(xiàn)了負(fù)數(shù)呢U角铩!旁振!
由此,怪物出現(xiàn)啦涨岁!我們今天就來(lái)解讀(xiang fu)一下它拐袜!
原因分析+知識(shí)惡補(bǔ)
想到這里,我們一定就會(huì)怪是位運(yùn)算的鍋梢薪!那這個(gè)鍋該怎么讓位運(yùn)算背起來(lái)呢蹬铺!我們來(lái)研究一下!
首先我們知道秉撇,JS中沒有真正的整型甜攀,數(shù)據(jù)都是以double(64bit)的標(biāo)準(zhǔn)格式存儲(chǔ)的秋泄,這里就不再贅述了,要想搞透其中的原理规阀,請(qǐng)打開【傳送門】
-
什么是位運(yùn)算恒序?
位運(yùn)算是在數(shù)字底層(即表示數(shù)字的 32 個(gè)數(shù)位)進(jìn)行運(yùn)算的。由于位運(yùn)算是低級(jí)的運(yùn)算操作谁撼,所以速度往往也是最快的(相對(duì)其它運(yùn)算如加減乘除來(lái)說)歧胁,并且借助位運(yùn)算有時(shí)我們還能實(shí)現(xiàn)更簡(jiǎn)單的程序邏輯,缺點(diǎn)是很不直觀,許多場(chǎng)合不能夠使用厉碟。
位運(yùn)算只對(duì)整數(shù)起作用喊巍,如果一個(gè)運(yùn)算子不是整數(shù),會(huì)自動(dòng)轉(zhuǎn)為整數(shù)后再運(yùn)行箍鼓。雖然在 JavaScript 內(nèi)部崭参,數(shù)值都是以64位浮點(diǎn)數(shù)的形式儲(chǔ)存,但是做位運(yùn)算的時(shí)候款咖,是以32位帶符號(hào)的整數(shù)進(jìn)行運(yùn)算的何暮,并且返回值也是一個(gè)32位帶符號(hào)的整數(shù)。而32位整型的取值范圍是:
-2147483648 到 2147483647
之剧,最大值轉(zhuǎn)換為時(shí)間后是:2038-01-19 11:14:07
郭卫,所以我們?cè)谶@里做個(gè)預(yù)測(cè),2038年1月19號(hào)11:14:07后背稼,如果地球還在贰军,可能會(huì)有一批程序出現(xiàn)BUG。顯然10位時(shí)間戳目前還在這個(gè)范圍內(nèi)蟹肘,而超過10位后就已經(jīng)超出了界限词疼,這也就是為什么會(huì)因?yàn)槲贿\(yùn)算導(dǎo)致精度丟失的原因所在了。
-
關(guān)于二進(jìn)制
- ECMAScript中的所有數(shù)值都以IEEE-754 64位格式存儲(chǔ)帘腹,但位操作符并不直接操作64位的值贰盗,而是以32位帶符號(hào)的整數(shù)進(jìn)行運(yùn)算的,并且返回值也是一個(gè)32位帶符號(hào)的整數(shù)
- 這種位數(shù)轉(zhuǎn)換使得在對(duì)特殊的NaN和Infinity值應(yīng)用位操作時(shí)阳欲,這兩個(gè)值都會(huì)被當(dāng)成0來(lái)處理
- 如果對(duì)非數(shù)值應(yīng)用位操作符舵盈,會(huì)先使用Number()將該值轉(zhuǎn)換成數(shù)值再應(yīng)用位操作,得到的結(jié)果是一個(gè)數(shù)值
//'|'表示按位或球化,一個(gè)整數(shù)與0按位或運(yùn)算可以得到它本身秽晚,一個(gè)小數(shù)與0按位或運(yùn)算可以得到取整效果
console.log( 1.3 | 0);//1
console.log( 1.8 | 0);//1
console.log( Infinity | 0);//0
console.log( -Infinity | 0);//0
console.log( NaN | 0);//0
console.log('12px' | 0);//0
console.log('12' | 0);//12
以下來(lái)源于w3shool:
ECMAScript 整數(shù)有兩種類型,即有符號(hào)整數(shù)(允許用正數(shù)和負(fù)數(shù))和無(wú)符號(hào)整數(shù)(只允許用正數(shù))筒愚。在 ECMAScript 中赴蝇,所有整數(shù)字面量默認(rèn)都是有符號(hào)整數(shù),這意味著什么呢巢掺?
有符號(hào)整數(shù)使用 31 位表示整數(shù)的數(shù)值句伶,用第 32 位表示整數(shù)的符號(hào)劲蜻,0 表示正數(shù),1 表示負(fù)數(shù)考余。數(shù)值范圍從 -2147483648 到 2147483647
先嬉。
可以以兩種不同的方式存儲(chǔ)二進(jìn)制形式的有符號(hào)整數(shù),一種用于存儲(chǔ)正數(shù)秃殉,一種用于存儲(chǔ)負(fù)數(shù)坝初。正數(shù)是以真二進(jìn)制形式存儲(chǔ)的,前 31 位中的每一位都表示 2 的冪钾军,從第 1 位(位 0)開始鳄袍,表示 20,第 2 位(位 1)表示 21吏恭。沒用到的位用 0 填充拗小,即忽略不計(jì)。例如樱哼,下圖展示的是數(shù) 18 的表示法哀九。
那在js中二進(jìn)制和十進(jìn)制如何轉(zhuǎn)換呢?如下
console.log((18).toString(2));//"10010"
console.log(0b00000000000000000000000000010010);//18
// 十進(jìn)制 => 二進(jìn)制
let num = 10;
console.log(num.toString(2));
// 二進(jìn)制 => 十進(jìn)制
let num1 = 1001;
console.log(parseInt(num1, 2));
負(fù)數(shù)同樣以二進(jìn)制存儲(chǔ)搅幅,但使用的格式是二進(jìn)制補(bǔ)碼阅束。計(jì)算一個(gè)數(shù)值的二進(jìn)制補(bǔ)碼,需要經(jīng)過下列3個(gè)步驟:
- 求這個(gè)數(shù)值絕對(duì)值的二進(jìn)制碼
- 求二進(jìn)制反碼茄唐,即將0替換成1息裸,將1替換成0
- 得到的二進(jìn)制反碼加1
例如,要確定-18的二進(jìn)制表示沪编,首先必須得到18的二進(jìn)制表示呼盆,如下所示:
0000 0000 0000 0000 0000 0000 0001 0010
接下來(lái),計(jì)算二進(jìn)制反碼蚁廓,如下所示:
1111 1111 1111 1111 1111 1111 1110 1101
最后访圃,在二進(jìn)制反碼上加 1,如下所示:
1111 1111 1111 1111 1111 1111 1110 1101 +
0000000000000000000000000000 0001 =
1111 1111 1111 1111 1111 1111 1110 1110
因此相嵌,-18 的二進(jìn)制就是 1111 1111 1111 1111 1111 1111 1110 1110
而其相反數(shù)18的二進(jìn)制為0000 0000 0000 0000 0000 0000 0001 0010
ECMAScript會(huì)盡力向我們隱藏所有這些信息腿时,在以二進(jìn)制字符串形式輸出一個(gè)負(fù)數(shù)時(shí),我們看到的只是這個(gè)負(fù)數(shù)絕對(duì)值的二進(jìn)制碼前面加上了一個(gè)負(fù)號(hào)
var num = -18;
console.log(num.toString(2));//'-10010'
-
浮點(diǎn)數(shù)的二進(jìn)制
JavaScript 只有一種數(shù)字類型 ( Number )
JavaScript采用 IEEE 754 標(biāo)準(zhǔn)雙精度浮點(diǎn)(double64)饭宾,64位中有1位符號(hào)位批糟,11位存儲(chǔ)指數(shù),52位存儲(chǔ)浮點(diǎn)數(shù)的有效數(shù)字
有時(shí)候小數(shù)在二進(jìn)制中表示是無(wú)限的捏雌,所以從53位開始就會(huì)舍入(舍入規(guī)則是0舍1入)跃赚,這樣就造成了“浮點(diǎn)精度問題”(由于舍入規(guī)則有時(shí)大點(diǎn)笆搓,有時(shí)小點(diǎn))
IEEE標(biāo)準(zhǔn)中float的存儲(chǔ)規(guī)則
IEEE標(biāo)準(zhǔn)中double的存儲(chǔ)規(guī)則
更多詳細(xì)介紹性湿,請(qǐng)參看傳送門
我們將1596596596.3742654.toString(2)
轉(zhuǎn)為二進(jìn)制字符串表示如下:
1011111001010100010000101110100.0101111111001111110111
但實(shí)際在內(nèi)存中的存儲(chǔ)如下:
- 首先將整數(shù)部分
1596596596
轉(zhuǎn)為二進(jìn)制:1011111001010100010000101110100
- 將小數(shù)部分轉(zhuǎn)為二進(jìn)制:
0.010111111100111111011011011101010000011000111100010111
- 所以其二進(jìn)制拼接后為:
1011111001010100010000101110100.010111111100111111011011011101010000011000111100010111
纬傲,但顯然位數(shù)超出了64位的限制,而且小數(shù)點(diǎn)也不可能存儲(chǔ)的為小數(shù)點(diǎn)(只有0和1胺羝怠) - 所以將小數(shù)點(diǎn)左移30位后轉(zhuǎn)為科學(xué)計(jì)數(shù)法:
1.011111001010100010000101110100010111111100111111011011011101010000011000111100010111 * 2^30
- 正數(shù)叹括,符號(hào)位為0,我們?cè)谧罡呶环?hào)位中填0
- 指數(shù)部分宵荒,通過左移得到的汁雷,指數(shù)為正,因此62位填1报咳,然后將指數(shù)
30-1=29
侠讯,二進(jìn)制為101001,在左邊添0暑刃,所以61~52位湊夠了10位厢漩,因此指數(shù)部分為100 0010 1001
- 至于尾數(shù)部分,直接將科學(xué)計(jì)數(shù)法后小數(shù)點(diǎn)后面的數(shù)扔進(jìn)去即可(因?yàn)槌?2位長(zhǎng)度岩臣,所以更多的位數(shù)會(huì)舍去溜嗜,最后一位會(huì)0舍1入),所以尾數(shù)部分為:
0111110010101000100001011101000101111111001111110111
- 至此架谎,這個(gè)浮點(diǎn)數(shù)的二進(jìn)制就存儲(chǔ)為:
0100 0010 1001 0111 1100 1010 1000 1000 0101 1101 0001 0111 1111 0011 1111 0111
炸宵,轉(zhuǎn)為16進(jìn)制為:0x4297CA885D17F3F7
-
JS中的精度丟失
說到這里就不得不簡(jiǎn)單提一下數(shù)字精度丟失的問題。上面也知道谷扣,JS中所有的數(shù)字都是用double方式進(jìn)行存儲(chǔ)的土全,所以必然會(huì)存在精度丟失問題。
以下轉(zhuǎn)自文章:JavaScript數(shù)字精度丟失問題總結(jié)
此時(shí)只能模仿十進(jìn)制進(jìn)行四舍五入了抑钟,但是二進(jìn)制只有 0 和 1 兩個(gè)涯曲,于是變?yōu)?0 舍 1 入。這即是計(jì)算機(jī)中部分浮點(diǎn)數(shù)運(yùn)算時(shí)出現(xiàn)誤差在塔,丟失精度的根本原因幻件。
大整數(shù)的精度丟失和浮點(diǎn)數(shù)本質(zhì)上是一樣的,尾數(shù)位最大是 52 位蛔溃,因此 JS 中能精準(zhǔn)表示的最大整數(shù)是 Math.pow(2, 53)
绰沥,十進(jìn)制即 9007199254740992
大于9007199254740992
的可能會(huì)丟失精度:
9007199254740992 >> 10000000000000...000 ``// 共計(jì) 53 個(gè) 0
9007199254740992 + 1 >> 10000000000000...001 ``// 中間 52 個(gè) 0
9007199254740992 + 2 >> 10000000000000...010 ``// 中間 51 個(gè) 0
實(shí)際上
9007199254740992 + 1 ``// 丟失
9007199254740992 + 2 ``// 未丟失
9007199254740992 + 3 ``// 丟失
9007199254740992 + 4 ``// 未丟失
以上,可以知道看似有窮的數(shù)字, 在計(jì)算機(jī)的二進(jìn)制表示里卻是無(wú)窮的贺待,由于存儲(chǔ)位數(shù)限制因此存在“舍去”徽曲,精度丟失就發(fā)生了。
想了解更深入的分析可以看這篇論文(你品麸塞!你細(xì)品M撼肌):What Every Computer Scientist Should Know About Floating-Point Arithmetic
關(guān)于精度和范圍的內(nèi)容可查看【JS的數(shù)值精度和數(shù)值范圍】
位運(yùn)算導(dǎo)致數(shù)據(jù)異常的過程分析
通過前面的知識(shí)補(bǔ)充,我們已經(jīng)知道:
位運(yùn)算只對(duì)整數(shù)起作用,如果一個(gè)運(yùn)算子不是整數(shù)奥此,會(huì)自動(dòng)轉(zhuǎn)為整數(shù)后再運(yùn)行弧哎。雖然在 JavaScript 內(nèi)部,數(shù)值都是以64位浮點(diǎn)數(shù)的形式儲(chǔ)存稚虎,但是做位運(yùn)算的時(shí)候撤嫩,是以32位帶符號(hào)的整數(shù)進(jìn)行運(yùn)算的,并且返回值也是一個(gè)32位帶符號(hào)的整數(shù)蠢终。
ECMAScript 中序攘,所有整數(shù)字面量默認(rèn)都是有符號(hào)整數(shù),這意味著什么呢寻拂?有符號(hào)整數(shù)使用 31 位表示整數(shù)的數(shù)值程奠,用第 32 位表示整數(shù)的符號(hào),0 表示正數(shù)祭钉,1 表示負(fù)數(shù)梦染。數(shù)值范圍從
-2147483648 到 2147483647
。
這也就是為什么對(duì)于整數(shù)部位為10位的時(shí)間戳朴皆,通過位運(yùn)算可以進(jìn)行取整(因?yàn)槟壳皶r(shí)間戳159xxxxxxx<2147483647)帕识,不存在時(shí)間戳超過范圍的問題。但是對(duì)于13位時(shí)間戳遂铡,如1596615447123>2147483647
肮疗,此時(shí)再通過位運(yùn)算操作的時(shí)候就會(huì)導(dǎo)致異常,如:
let t = 1596615447015.007
console.log(Math.trunc(t), Math.trunc(t / 1000)) // 1596615447015 1596615447
console.log(t / 1000 | 0) // 1596615447
console.log(t | 0) // -1112387097
這主要是因?yàn)樵谶M(jìn)行位運(yùn)算之前扒接,JS會(huì)先將64bit的浮點(diǎn)數(shù)1596615447015.01
轉(zhuǎn)為32bit的有符號(hào)整型后進(jìn)行運(yùn)算的伪货,這個(gè)轉(zhuǎn)換過程如下:
- 首先
1596615447015.333
的二進(jìn)制表示為10111001110111101101100100101000111100111.0101010101
,其在內(nèi)存中的存儲(chǔ)結(jié)構(gòu)如下:- 正數(shù),最高位符號(hào)位
0
- 科學(xué)計(jì)數(shù)法小數(shù)點(diǎn)左移钾怔,指數(shù)位最高位為
1
- 小數(shù)點(diǎn)左移40位碱呼,則剩余指數(shù)部分為
40-1=39
的10位二進(jìn)制00 0010 0111
- 所以前12位為
0100 0010 0111
- 正數(shù),最高位符號(hào)位
- 剩余52位從小數(shù)點(diǎn)后開始取52位(不足52位在最后補(bǔ)0,超過則最后一位0舍1入)為
0111001110111101101100100101000111100111010101010100
- 所以
1596615447015.333
的二進(jìn)制存儲(chǔ)表示為:0100 0010 0111 0111 0011 1011 1101 1011 0010 0101 0001 1110 0111 0101 0101 0100
宗侦,轉(zhuǎn)為16進(jìn)制表示為:0x42773BDB251E7554
- 開始將其轉(zhuǎn)為32bit的int類型愚臀,首先根據(jù)指數(shù)位
100 0010 0111
可知,小數(shù)點(diǎn)右移39+1=40位矾利,剩余小數(shù)位數(shù)舍掉姑裂,則52位尾數(shù)部分得到的是73BDB251E7
,即二進(jìn)制表示為0111 0011 1011 1101 1011 0010 0101 0001 1110 0111
- 截取上面二進(jìn)制的后32位得到:
1011 1101 1011 0010 0101 0001 1110 0111
男旗,系統(tǒng)會(huì)將這32位數(shù)當(dāng)作轉(zhuǎn)換后的int類型舶斧,由于最高位為1
,即這是一個(gè)負(fù)數(shù) - 對(duì)于系統(tǒng)來(lái)說察皇,如果是負(fù)數(shù)茴厉,則用這個(gè)負(fù)數(shù)的補(bǔ)碼表示,即這個(gè)負(fù)數(shù)絕對(duì)值的二進(jìn)制按位取反,然后最后一位執(zhí)行不進(jìn)位+1的來(lái)的矾缓,所以對(duì)于上面這個(gè)二進(jìn)制师痕,將其轉(zhuǎn)為10進(jìn)制的過程如下:
- 最高位符號(hào)位為1,表示負(fù)數(shù)
- 既然是負(fù)數(shù)而账,最后一位不退位-1,得到:
011 1101 1011 0010 0101 0001 1110 0110
- 取補(bǔ)碼:
100 0010 0100 1101 1010 1110 0001 1001
- 表示為十進(jìn)制:
-1112387097
- 至此因篇,就可以解釋為什么
1596615447015.333 | 0 = -1112387097
了泞辐。
為了驗(yàn)證上述過程,我們?cè)倥e一個(gè)例子:1590015447015.123 >> 0 = 877547495
-
1590015447015.123
的二進(jìn)制表示為:10111001000110100010011100100111111100111.000111111
- 舍去其小數(shù)部分后竞滓,從后往前取32位為:
00110100010011100100111111100111
- 最高位為0咐吼,正數(shù),直接轉(zhuǎn)為10進(jìn)制為:
877547495
將將將將商佑!沒錯(cuò)的吧锯茄!所以JS的這個(gè)坑還真是。茶没。肌幽。 讓人Orz