摘要:原碼補碼和反碼原碼一個數在計算機中是以二進制的形式存在的,其中第一位存放符號正數為負數為。中的位運算在中按位操作符會將其操作數轉成補碼形式的有符號位整數。原文鏈接由扯到中的位運算
這個話題的由來是2016年3月份的時候 NPM 社區發生了‘left-pad’事件,不久后社區就有人發布了用來補救的,也是現在大家能用到的 left-pad 庫。
最開始這個庫的代碼是這樣的。
module.exports = leftpad; function leftpad (str, len, ch) { str = String(str); var i = -1; if (!ch && ch !== 0) ch = " "; len = len - str.length; while (++i < len) { str = ch + str; } return str; }
我第一次看到這段代碼的時候,沒看出什么毛病,覺得清晰明了。后來刷微博的時候@左耳朵耗子老師指出了這段代碼可以寫得更有效率些,于是他就貼出了自己寫的版本并給 left-pad 提了 PR,代碼如下:
module.exports = leftpad; function leftpad (str, len, ch) { //convert the `str` to String str = str +""; //needn"t to pad len = len - str.length; if (len <= 0) return str; //convert the `ch` to String if (!ch && ch !== 0) ch = " "; ch = ch + ""; var pad = ""; while (true) { if (len & 1) pad += ch; len >>= 1; if (len) ch += ch; else break; } return pad + str; }
我當時看到他的這段代碼的里面的 &和>>運算符的時候一下有點懵了,只知道這是位運算里面的‘按位與’和‘右移’運算,但是完全不知道為什么這樣寫就能提高效率。于是就想著去了解位運算的實質和使用場景。
在了解位運算之前,我們先必須了解一下什么是原碼、反碼和補碼以及二進制與十進制的轉換。
原碼、補碼和反碼 原碼一個數在計算機中是以二進制的形式存在的,其中第一位存放符號, 正數為0, 負數為1。原碼就是用第一位存放符號的二進制數值。例如2的原碼為00000010,-2的原碼為10000010。
反碼正數的反碼是其本身。負數的反碼是在其原碼的基礎上,符號位不變,其余各位取反,即0變1,1變0。
[+3]=[00000011]原=[00000011]反 [-3]=[10000011]原=[11111100]反
可見如果一個反碼表示的是負數,并不能直觀的看出它的數值,通常要將其轉換成原碼再計算。
補碼正數的補碼是其本身。負數的補碼是在其原碼的基礎上,符號位不變,其余各位取反,最后+1。(即負數的補碼為在其反碼的基礎上+1)。
[+3]=[00000011]原=[00000011]反=[00000011]補 [-3]=[10000011]原=[11111100]反=[11111101]補
可見對于負數,補碼的表示方式也是讓人無法直觀看出其數值的,通常也需要轉換成原碼再計算。
二進制與十進制的轉換二進制與十進制的區別在于數運算時是逢幾進一位。二進制是逢2進一位,十進制也就是我們常用的0-9是逢10進一位
正整數的十進制轉二進制正整數的十進制轉二進制的方法為將一個十進制數除以2,得到的商再除以2,以此類推直到商等于1或0時為止,倒取除得的余數,即為轉換所得的二進制數的結果。
例如把52換算成二進制數,計算過程如下圖:
52除以2得到的余數依次為:0、0、1、0、1、1,倒序排列,所以52對應的二進制數就是110100。
負整數的十進制轉二進制為將該負整數對應的正整數先轉換成二進制,然后對其“取反”,再對取反后的結果+1。即負整數采用其二進制補碼的形式存儲。
至于負數為什么要用二進制補碼的形式存儲,可參考一篇阮一峰的文章《關于2的補碼》。
例如 -52 的原碼為 10110100,其反碼為 11001011,其補碼為 11001100。所以 -52 轉換為二進制后為 11001100。
十進制小數轉二進制的方法為“乘2取整”,對十進制小數乘2得到的整數部分和小數部分,整數部分即是相應的二進制數碼,再用2乘小數部分(之前乘后得到新的小數部分),又得到整數和小數部分。
如此不斷重復,直到小數部分為0或達到精度要求為止。第一次所得到為最高位,最后一次得到為最低位。
如:0.25的二進制 0.25*2=0.5 取整是0 0.5*2=1.0 取整是1 即0.25的二進制為 0.01 ( 第一次所得到為最高位,最后一次得到為最低位) 0.8125的二進制 0.8125*2=1.625 取整是1 0.625*2=1.25 取整是1 0.25*2=0.5 取整是0 0.5*2=1.0 取整是1 即0.8125的二進制是0.1101二進制轉十進制
從最后一位開始算,依次列為第0、1、2...位,第n位的數(0或1)乘以2的n次方,將得到的結果相加就是得到的十進制數。
例如二進制為110的數,將其轉為十進制的過程如下
個位數 0 與 2o 相乘:0 × 2o = 0
十位數 1 與 21 相乘:1 × 21 = 2
百位數 1 與 22 相乘:1 × 22 = 4
將得到的結果相加:0+2+4=6
所以二進制 110 轉換為十進制后的數值為 6。
小數二進制用數值乘以2的負冪次然后相加。
JavaScript 中的位運算在 ECMAScript 中按位操作符會將其操作數轉成補碼形式的有符號32位整數。下面是ECMAScript 規格中對于位運算的執行過程的表述:
The production A : A @ B, where @ is one of the bitwise operators in the productions above, is evaluated as follows: 1. Let lref be the result of evaluating A. 2. Let lval be GetValue(lref). 3. ReturnIfAbrupt(lval). 4. Let rref be the result of evaluating B. 5. Let rval be GetValue(rref). 6. ReturnIfAbrupt(rval). 7. Let lnum be ToInt32(lval). 8. ReturnIfAbrupt(lnum). 9. Let rnum be ToInt32(rval). 10. ReturnIfAbrupt(rnum). 11. Return the result of applying the bitwise operator @ to lnum and rnum. The result is a signed 32 bit integer.
需要注意的是第七步和第九步,根據 ES 的標準,超過32位的整數會被截斷,而小數部分則會被直接舍棄。所以由此可以知道,在 JS 中,當位運算中有操作數大于或等于232時,就會出現意想不到的結果。
JavaScript 中的位運算有:&(按位與)、|(按位或)、~(取反)、^(按位異或)、<<(左移)、>>(有符號右移)和>>>(無符號右移)。
&按位與對每一個比特位執行與(AND)操作。只有 a 和 b 都是 1 時,a & b 才是 1。
例如:9(base 10) & 14(base 10) = 1001(base2) & 1110(base 2) = 1000(base 2) = 8(base 10)
因為當只有 a 和 b 都是 1 時,a&b才等于1,所以任一數值 x 與0(二進制的每一位都是0)按位與操作,其結果都為0。將任一數值 x 與 -1(二進制的每一位都是1)按位與操作,其結果都為 x。
利用 & 運算的特點,我們可以用以簡單的判斷奇偶數,公式:
(n & 1) === 0 //true 為偶數,false 為奇數。
因為 1 的二進制只有最后一位為1,其余位都是0,所以其判斷奇偶的實質是判斷二進制數最后一位是 0 還是 1。奇數的二進制最后一位是 1,偶數是0。
當然還可以利用 JS 在做位運算時會舍棄掉小數部分的特性來做向下取整的運算,因為當 x 為整數時有 x&-1=x,所以當 x 為小數時有 x&-1===Math.floor(x)。
|按位或對每一個比特位執行或(OR)操作。如果 a 或 b 為 1,則 a | b 結果為 1。
例如:9(base 10) | 14(base 10) = 1001(base2) | 1110(base 2) = 1111(base 2) = 15(base 10)
因為只要 a 或 b 其中一個是 1 時,a|b就等于1,所以任一數值 x 與-1(二進制的每一位都是1)按位與操作,其結果都為-1。將任一數值 x 與 0(二進制的每一位都是0)按位與操作,其結果都為 x。
同樣,按位或也可以做向下取整運算,因為當 x 為整數時有 x|0=x,所以當 x 為小數時有 x|0===Math.floor(x)。
~取反對每一個比特位執行非(NOT)操作。~a 結果為 a 的反轉(即反碼)。
9 (base 10) = 00000000000000000000000000001001 (base 2) -------------------------------- ~9 (base 10) = 11111111111111111111111111110110 (base 2) = -10 (base 10)
負數的二進制轉化為十進制的規則是,符號位不變,其他位取反后加 1。
對任一數值 x 進行按位非操作的結果為 -(x + 1)。~~x === x。
同樣,取反也可以做向下取整運算,因為當 x 為整數時有 ~~x===x,所以當 x 為小數時有 ~~x===Math.floor(x)。
^按位異或對每一對比特位執行異或(XOR)操作。當 a 和 b 不相同時,a ^ b 的結果為 1。
例如:9(base 10) ^ 14(base 10) = 1001(base2) ^ 1110(base 2) = 0111(base 2) = 7(base 10)
將任一數值 x 與 0 進行異或操作,其結果為 x。將任一數值 x 與 -1 進行異或操作,其結果為 ~x,即 x^-1=~x。
同樣,按位異或也可以做向下取整運算,因為當 x 為整數時有 (x^0)===x,所以當 x 為小數時有 (x^0)===Math.floor(x)。
它把數字中的所有數位向左移動指定的數量,向左被移出的位被丟棄,右側用 0 補充。
例如,把數字 2(等于二進制中的 10)左移 5 位,結果為 64(等于二進制中的 1000000):
var iOld = 2; //等于二進制 10 var iNew = iOld << 5; //等于二進制 1000000 十進制 64
因為二進制10轉換成十進制的過程為 1×21+0×2o,在運算中2的指數與位置數相對應,當左移五位后就變成了 1×21??+0×2o??= 1×21×2?+0×2o×2? = (1×21+0×2o)×2?。所以由此可以看出當2左移五位就變成了 2×2?=64。
所以有一個數左移 n 為,即為這個數乘以2的n次方。x<
同樣,左移運算也可以做向下取整運算,因為當 x 為整數時有 (x<<0)===x,所以當 x 為小數時有 (x<<0)===Math.floor(x)。
它把 32 位數字中的所有數位整體右移,同時保留該數的符號(正號或負號)。有符號右移運算符恰好與左移運算相反。例如,把 64 右移 5 位,將變為 2。
因為有符號右移運算符與左移運算相反,所以有一個數左移 n 為,即為這個數除以2的n次方。x<
同樣,有符號右移運算也可以做向下取整運算,因為當 x 為整數時有 (x>>0)===x,所以當 x 為小數時有 (x>>0)===Math.floor(x)。
它將無符號 32 位數的所有數位整體右移。對于正數,無符號右移運算的結果與有符號右移運算一樣,而負數則被作為正數來處理。
-9 (base 10): 11111111111111111111111111110111 (base 2) -------------------------------- -9 >>> 2 (base 10): 00111111111111111111111111111101 (base 2) = 1073741821 (base 10)
根據無符號右移的正數右移與有符號右移運算一樣,而負數的無符號右移一定為非負的特征,可以用來判斷數字的正負,如下:
function isPos(n) { return (n === (n >>> 0)) ? true : false; } isPos(-1); // false isPos(1); // true總結
根據 JS 的位運算,可以得出如下信息:
1、所有的位運算都可以對小數取底。
2、對于按位與&,可以用 (n & 1) === 0 //true 為偶數,false 為奇數。來判斷奇偶。用x&-1===Math.floor(x)來向下取底。
3、對于按位或|,可以用x|0===Math.floor(x)來向下取底。
4、對于取反運算~,可以用~~x===Math.floor(x)來向下取底。
5、對于異或運算^,可以用(x^0)===Math.floor(x)來向下取底。
6、對于左移運算<<,可以x<
7、對于有符號右移運算>>,可以x<
8、對于無符號右移運算>>>,可以(n === (n >>> 0)) ? true : false;來判斷數字正負,用x>>>0===Math.floor(x)來向下取底。
用移位運算來替代普通算術能獲得更高的效率。移位運算翻譯成機器碼的長度更短,執行更快,需要的硬件開銷更小。
原文鏈接:由left-pad扯到JS中的位運算
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/88865.html
摘要:雖然在內部,數值都是以位浮點數的形式儲存,但是做位運算的時候,是以位帶符號的整數進行運算的,并且返回值也是一個位帶符號的整數。如下表應用場景取整對于一般的整數,返回值不會有任何變化。例如,結果為負數存儲采用的形式是二進制補碼。 什么是位運算? 位運算是在數字底層(即表示數字的 32 個數位)進行運算的。由于位運算是低級的運算操作,所以速度往往也是最快的(相對其它運算如加減乘除來說),并...
摘要:雖然在內部,數值都是以位浮點數的形式儲存,但是做位運算的時候,是以位帶符號的整數進行運算的,并且返回值也是一個位帶符號的整數。如下表應用場景取整對于一般的整數,返回值不會有任何變化。例如,結果為負數存儲采用的形式是二進制補碼。 什么是位運算? 位運算是在數字底層(即表示數字的 32 個數位)進行運算的。由于位運算是低級的運算操作,所以速度往往也是最快的(相對其它運算如加減乘除來說),并...
摘要:雖然在內部,數值都是以位浮點數的形式儲存,但是做位運算的時候,是以位帶符號的整數進行運算的,并且返回值也是一個位帶符號的整數。如下表應用場景取整對于一般的整數,返回值不會有任何變化。例如,結果為負數存儲采用的形式是二進制補碼。 什么是位運算? 位運算是在數字底層(即表示數字的 32 個數位)進行運算的。由于位運算是低級的運算操作,所以速度往往也是最快的(相對其它運算如加減乘除來說),并...
摘要:也就是說不僅是會產生這種問題,只要是采用的浮點數編碼方式來表示浮點數時,則會產生這類問題。到這里我們都理解只要采取的浮點數編碼的語言均會出現上述問題,只是它們的標準類庫已經為我們提供了解決方案而已。 Brief 一天有個朋友問我JS中計算0.7 * 180怎么會等于125.99999999998,坑也太多了吧!那時我猜測是二進制表示數值時發生round-off error所導致,但并不...
摘要:作者源地址最近一個事件搞得圈沸沸揚揚的我們暫且把這個事情放一邊來看看本身的實現的源碼如下這段程序的作用是給一個字符串或可以轉成的變量用字符在左邊補位將其補到長度為當然這個程序沒做充足的參數檢查這個就不細說了我們分析一下它的效率如果 作者: @flowmemo源地址: http://flowmemo.github.io/2016/03/25/str... 最近一個left-pad事件搞得...
閱讀 1206·2021-11-24 09:39
閱讀 2129·2021-11-22 13:54
閱讀 2111·2021-09-08 10:45
閱讀 1443·2021-08-09 13:43
閱讀 2985·2019-08-30 15:52
閱讀 3083·2019-08-29 15:38
閱讀 2848·2019-08-26 13:44
閱讀 3055·2019-08-26 13:30