由left-pad扯到JS中的位運算

這個話題的由來是2016年3月份的時候 NPM 社區發生了‘left-pad’事件,不久後社區就有人發佈了用來補救的,也是如今你們能用到的 left-pad 庫。javascript

最開始這個庫的代碼是這樣的。html

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,代碼以下:java

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;
}

我當時看到他的這段代碼的裏面的 &>>運算符的時候一下有點懵了,只知道這是位運算裏面的‘按位與’和‘右移’運算,可是徹底不知道爲何這樣寫就能提升效率。因而就想着去了解位運算的實質和使用場景。npm

在瞭解位運算以前,咱們先必須瞭解一下什麼是原碼、反碼和補碼以及二進制與十進制的轉換。app

原碼、補碼和反碼

原碼

一個數在計算機中是以二進制的形式存在的,其中第一位存放符號, 正數爲0, 負數爲1。原碼就是用第一位存放符號的二進制數值。例如2的原碼爲00000010,-2的原碼爲10000010。lua

反碼

正數的反碼是其自己。負數的反碼是在其原碼的基礎上,符號位不變,其他各位取反,即0變1,1變0。spa

[+3]=[00000011]原=[00000011]反
[-3]=[10000011]原=[11111100]反

可見若是一個反碼錶示的是負數,並不能直觀的看出它的數值,一般要將其轉換成原碼再計算。翻譯

補碼

正數的補碼是其自己。負數的補碼是在其原碼的基礎上,符號位不變,其他各位取反,最後+1。(即負數的補碼爲在其反碼的基礎上+1)。code

[+3]=[00000011]原=[00000011]反=[00000011]補
[-3]=[10000011]原=[11111100]反=[11111101]補

可見對於負數,補碼的表示方式也是讓人沒法直觀看出其數值的,一般也須要轉換成原碼再計算。htm

二進制與十進制的轉換

二進制與十進制的區別在於數運算時是逢幾進一位。二進制是逢2進一位,十進制也就是咱們經常使用的0-9是逢10進一位

正整數的十進制轉二進制

正整數的十進制轉二進制的方法爲將一個十進制數除以2,獲得的商再除以2,以此類推直到商等於1或0時爲止,倒取除得的餘數,即爲轉換所得的二進制數的結果。

例如把52換算成二進制數,計算過程以下圖:
10to2
52除以2獲得的餘數依次爲:0、0、一、0、一、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、一、2...位,第n位的數(0或1)乘以2的n次方,將獲得的結果相加就是獲得的十進制數。
例如二進制爲110的數,將其轉爲十進制的過程以下
2to10
個位數 0 與 2º 相乘:0 × 2º = 0
十位數 1 與 2¹ 相乘:1 × 2¹ = 2
百位數 1 與 2² 相乘:1 × 2² = 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 中,當位運算中有操做數大於或等於2³²時,就會出現意想不到的結果。

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×2¹+0×2º,在運算中2的指數與位置數相對應,當左移五位後就變成了 1×2¹⁺⁵+0×2º⁺⁵= 1×2¹×2⁵+0×2º×2⁵ = (1×2¹+0×2º)×2⁵。因此由此能夠看出當2左移五位就變成了 2×2⁵=64。
因此有一個數左移 n 爲,即爲這個數乘以2的n次方。x<<n === x*2ⁿ
一樣,左移運算也能夠作向下取整運算,由於當 x 爲整數時有 (x<<0)===x,因此當 x 爲小數時有 (x<<0)===Math.floor(x)

>>有符號右移運算

它把 32 位數字中的全部數位總體右移,同時保留該數的符號(正號或負號)。有符號右移運算符剛好與左移運算相反。例如,把 64 右移 5 位,將變爲 2。
由於有符號右移運算符與左移運算相反,因此有一個數左移 n 爲,即爲這個數除以2的n次方。x<<n === x/2ⁿ
一樣,有符號右移運算也能夠作向下取整運算,由於當 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 的位運算,能夠得出以下信息:
一、全部的位運算均可以對小數取底。
二、對於按位與&,能夠用 (n & 1) === 0 //true 爲偶數,false 爲奇數。來判斷奇偶。用x&-1===Math.floor(x)來向下取底。
三、對於按位或|,能夠用x|0===Math.floor(x)來向下取底。
四、對於取反運算~,能夠用~~x===Math.floor(x)來向下取底。
五、對於異或運算^,能夠用(x^0)===Math.floor(x)來向下取底。
六、對於左移運算<<,能夠x<<n === x*2ⁿ來求2的n次方,用x<<0===Math.floor(x)來向下取底。
七、對於有符號右移運算>>,能夠x<<n === x/2ⁿ求一個數字的 N 等分,用x>>0===Math.floor(x)來向下取底。
八、對於無符號右移運算>>>,能夠(n === (n >>> 0)) ? true : false;來判斷數字正負,用x>>>0===Math.floor(x)來向下取底。

用移位運算來替代普通算術能得到更高的效率。移位運算翻譯成機器碼的長度更短,執行更快,須要的硬件開銷更小。

原文連接:由left-pad扯到JS中的位運算

相關文章
相關標籤/搜索