全面解讀Math物件及位運算

路易斯發表於2017-03-22

Math方法和位運算幾乎是被忽略得最嚴重的知識點, 和正則一樣, 不用不知道, 一用到處查. 為了告別這種低效的程式設計模式, 我特地總結此篇, 系統梳理了這兩個知識點. 以此為冊, 助你攻破它們.

原文: louiszhai.github.io/2016/07/01/…

導讀

截至ES6, JavaScript 中內建(build-in)構造器/物件共有19個, 其中14個是構造器(Number,Boolean, String, Object, Function, Array, RegExp, Error, Date, Set, WeakSet, Map, Proxy, Promise), Global 不能直接訪問, Arguments僅在函式呼叫時由JS引擎建立, 而 Math, JSON, Reflect 是以物件形式存在的, 本篇將帶你走進 JS 內建物件-Math以及與之息息相關的位運算, 一探究竟.

為什麼Math這麼設計

眾所周知, 如果需要使用js進行一些常規的數學運算, 是一件十分麻煩的事情. 為了解決這個問題, ECMAScript 在1.1版本中便引入了 Math. Math 之所以被設計成一個物件, 而不是構造器, 是因為物件中的方法或屬性可以作為靜態方法或常量直接被呼叫, 方便使用, 同時, Math 也沒有建立例項的必要.

Math中的屬性

屬性名 描述
Math.E 尤拉常數,也是自然對數的底數 約2.718
Math.LN2 2的自然對數 約0.693
Math.LN10 10的自然對數 約2.303
Math.LOG2E 以2為底E的對數 約1.443
Math.LOG10E 以10為底E的對數 約0.434
Math.PI 圓周率 約3.14
Math.SQRT1_2 1/2的平方根 約0.707
Math.SQRT2 2的平方根 約1.414

Math中的方法

Math物件本就有很多用於運算的方法, 值得關注的是, ES6 規範又對Math物件做了一些擴充套件, 增加了一系列便捷的方法. 而這些方法大致可以分為以下三類.

三角函式

方法名 描述
Math.sin(x) 返回x的正弦值
Math.sinh(x) ES6新增 返回x的雙曲正弦值
Math.cos(x) 返回x的餘弦值
Math.cosh(x) ES6新增 返回x的雙曲餘弦值
Math.tan(x) 返回x的正切值
Math.tanh(x) ES6新增 返回x的雙曲正切值
Math.asin(x) 返回x的反正弦值
Math.asinh(x) ES6新增 返回x的反雙曲正弦值
Math.acos(x) 返回x的反餘弦值
Math.atan(x) 返回x的反正切值
Math.atan2(x, y) 返回 y/x 的反正切值
Math.atanh(x) ES6新增 返回 x 的反雙曲正切值

數學運算方法

方法名 描述 例子
Math.sqrt(x) 返回x的平方根 Math.sqrt(9);//3
Math.exp(x) 返回尤拉常數(e)的x次冪 Math.exp(1);//約2.718
Math.pow(x,y) 返回x的y次冪, 如果y未初始化, 則返回x Math.pow(2, 3);//8
Math.expm1(x) ES6新增 返回尤拉常數(e)的x次冪減去1的值 Math.exp(1);//約1.718
Math.log(x) 返回x的自然對數 Math.log(1);//0
Math.log1p(x) ES6新增 返回x+1後的自然對數 Math.log1p(0);//0
Math.log2(x) ES6新增 返回x以2為底的對數 Math.log2(8);//3
Math.log10(x) ES6新增 返回x以10為底的對數 Math.log10(100);//2
Math.cbrt(x) ES6新增 返回x的立方根 Math.cbrt(8);//約2
Math.clz32() ES6新增 返回一個數字在轉換成 32位無符號整型數字的二進位制形式後, 開頭的 0 的個數 Math.clz32(2);//30
Math.hypot(x,y,z) ES6新增 返回所有引數的平方和的平方根 Math.hypot(3,4);//5
Math.imul(x,y) ES6新增 返回兩個引數的類C的32位整數乘法運算的運算結果 Math.imul(0xffffffff, 5);//-5

數值運算方法

方法名 描述 例子
Math.abs(x) 返回x的絕對值 Math.abs(-5);//5
Math.floor(x) 返回小於x的最大整數 Math.floor(8.2);//8
Math.ceil(x) 返回大於x的最小整數 Math.ceil(8.2);//9
Math.trunc(x) ES6新增 返回x的整數部分 Math.trunc(1.23);//1
Math.fround(x) ES6新增 返回離它最近的單精度浮點數形式的數字 Math.fround(1.1);//1.100000023841858
Math.min(x,y,z) 返回多個數中的最小值 Math.min(3,1,5);//1
Math.max(x,y,z) 返回多個數中的最大值 Math.max(3,1,5);//5
Math.round(x) 返回四捨五入後的整數 Math.round(8.2);//8
Math.random() 返回0到1之間的偽隨機數 Math.random();
Math.sign(x) ES6新增 返回一個數的符號( 5種返回值, 分別是 1, -1, 0, -0, NaN. 代表的各是正數, 負數, 正零, 負零, NaN) Math.sign(-5);//-1

附:Number型別的數值運算方法

Number.prototype中有一個方法叫做toFixed(), 用於將數值裝換為指定小數位數的形式.

  • 沒有引數或者引數為零的情況下, toFixed() 方法返回該數值的四捨五入後的整數形式, 等同於 Math.round(x);
  • 其他情況下, 返回該數的指定小數位數的四捨五入後的結果.
var num = 1234.56789;
console.log(num.toFixed(),num.toFixed(0));//1235,1235
console.log(num.toFixed(1));//1234.6
console.log(-1.235.toFixed(2));//-1.24複製程式碼

Math方法的一些規律

以上, 數值運算中, 存在如下規律:

  1. Math.trunc(x) 方法當 ① x為正數時, 運算結果同 Math.floor(x); ② x為負數時, 運算結果同 Math.ceil(x). 實際上, 它完全可以由位運算替代, 且運算速度更快, 如 2.5&-1 或 2.5|0 或 ~~2.5 或 2.5^0 , 它們的運算結果都為2; 如 -2.5&-1 或 -2.5|0 或 ~~-2.5 或 -2.5^0 , 它們的運算結果都為-2;
  2. Math.min(x,y,z) 與 Math.max(x,y,z) 方法由於可接無限個引數, 可用於求陣列元素的最小最大值. 如: Math.max.apply(null,[5,3,8,9]); // 9 . 但是Math.min 不傳引數返回 Infinity, Math.max 不傳引數返回 -Infinity .
  3. 稍微利用 Math.random() 方法的特性, 就可以生成任意範圍的數字. 如: 生成10到80之間的隨機數, ~~(Math.random()*70 + 10);// 返回10~80之間的隨機數, 包含10不包含80

除去上述方法, Math作為物件, 繼承了來之Object物件的方法. 其中一些如下:

Math.valueOf();//返回Math物件本身
+Math; //NaN, 試圖轉換成數字,由於不能轉換為數字,返回NaN
Math.toString();//"[object Math]"複製程式碼

位運算

Math物件提供的方法種類繁多, 且覆蓋面非常全面, 基本上能夠滿足日常開發所需. 但同時我們也都知道, 使用Math物件的方法進行數值運算時, js程式碼經過解釋編譯, 最終會以二進位制的方式進行運算. 這種運算方式效率較低, 那麼能不能進一步提高運算的效率的呢? 如果我們使用位運算就可. 這是因為位運算本就是直接進行二進位制運算.

數值的二進位制值

由於位運算是基於二進位制的, 因此我們需要先獲取數值的二進位制值. 實際上, toString 方法已經幫我們做好了一部分工作, 如下:

//正整數可通過toString獲取
12..toString(2);//1100
//負整數問題就來了
(-12).toString(2);//-1100複製程式碼

已知: 負數在計算機內部是採用補碼錶示的. 例如 -1, 1的原碼是 0000 0001, 那麼1的反碼是 1111 1110, 補碼是 1111 1111.

故: 負數的十進位制轉換為二進位制時,符號位不變,其它位取反後+1. 即: -x的二進位制 = x的二進位制取反+1 . 由按位取反可藉助^運算子, 故負整數的二進位制可以藉助下面這個函式來獲取:

function getBinary(num){
  var s = (-num).toString(2),
      array = [].map.call(s,function(v){
        return v^1;
      });
  array.reduceRight(function(previousValue, value, index, array){
    var v = previousValue ^ value;
    array[index] = v;
    return +!v;
  },1);
  return array.join('');
}
getBinary(-12);//0100, 前面未補全的部分全部為1複製程式碼

然後, 多試幾次就會發現:

getBinary(-1) == 1..toString(2); //true
getBinary(-2) == 2..toString(2); //true
getBinary(-4) == 4..toString(2); //true
getBinary(-8) == 8..toString(2); //true複製程式碼

這表明:

  • 2的整數次方的值與它的相對數, 他們後面真正有效的那幾位都相同.

同樣, 負數的二進位制轉十進位制時, 符號位不變, 其他位取反後+1. 可參考:

function translateBinary2Decimal(binaryString){
  var array = [].map.call(binaryString,function(v){
    return v^1;
  });
  array.reduceRight(function(previousValue, value, index, array){
    var v = previousValue ^ value;
    array[index] = v;
    return +!v;
  },1);
  return parseInt(array.join(''),2);
}
translateBinary2Decimal(getBinary(-12));//12複製程式碼

由上, 二進位制轉十進位制和十進位制轉二進位制的函式, 大部分都可以共用, 因此下面提供一個統一的函式解決它們的互轉問題:

function translateBinary(item){
  var s = null,
      array = null,
      type = typeof item,
      symbol = !/^-/.test(item+'');
  switch(type){
    case "number": 
      s = Math.abs(item).toString(2);
      if(symbol){
        return s;
      }
      break;
    case "string":
      if(symbol){
        return parseInt(item,2);
      }
      s = item.substring(1);
      break;
    default:
      return false;
  }
  //按位取反
  array = [].map.call(s,function(v){
    return v^1;
  });
  //+1
  array.reduceRight(function(previousValue, value, index, array){
    var v = (previousValue + value)==2;
    array[index] = previousValue ^ value;
    return +v;
  },1);
  s = array.join('');
  return type=="number"?'-'+s:-parseInt(s,2);
}
translateBinary(-12);//"-0100"
translateBinary('-0100');//-12複製程式碼

常用的二進位制數

二進位制數 二進位制值
0xAAAAAAAA 10101010101010101010101010101010
0x55555555 01010101010101010101010101010101
0xCCCCCCCC 11001100110011001100110011001100
0x33333333 00110011001100110011001100110011
0xF0F0F0F0 11110000111100001111000011110000
0x0F0F0F0F 00001111000011110000111100001111
0xFF00FF00 11111111000000001111111100000000
0x00FF00FF 00000000111111110000000011111111
0xFFFF0000 11111111111111110000000000000000
0x0000FFFF 00000000000000001111111111111111

現在也可以使用上述方法來驗證下常用的二進位制值對不對. 如下:

translateBinary(0xAAAAAAAA);//"10101010101010101010101010101010"複製程式碼

按位與(&)

&運算子用於連線兩個數, 連線的兩個數它們二進位制補碼形式的值每位都將參與運算, 只有相對應的位上都為1時, 該位的運算才返回1. 比如 3 和 9 進行按位與運算, 以下是運算過程:

    0011    //3的二進位制補碼形式
&    1001    //9的二進位制補碼形式
--------------------
    0001    //1,相同位數依次運算,除最後一位都是1,返回1以外, 其它位數由於不同時為1都返回0複製程式碼

由上, 3&9的運算結果為1. 實際上, 由於按位與(&)運算同位上返回1的要求較為嚴苛, 因此, 它是一種趨向減小最大值的運算.(無論最大值是正數還是負數, 參與按位與運算後, 該數總是趨向減少二進位制值位上1的數量, 因此總是有值減小的趨勢. ) 對於按位與(&)運算, 滿足如下規律:

  1. 數值與自身(或者-1)按位與運算返回數值自身.
  2. 2的整數次方的值與它的相對數按位與運算返回它自身.
  3. 任意整數與0進行按位與運算, 都將會返回0.
  4. 任意整數與1進行按位與運算, 都只有0 或1 兩個返回值.
  5. 按位與運算的結果不大於兩數中的最大值.

由公式1, 我們可以對非整數取整. 即 x&x === x&-1 === Math.trunc(x) 如下:

console.log(5.2&5.2);//5
console.log(-5.2&-1);//-5
console.log(Math.trunc(-5.2)===(-5.2&-1));//true複製程式碼

由公式4, 我們可以由此判斷數值是否為奇數. 如下:

if(1 & x){//如果x為奇數,它的二進位制補碼形式最後一位必然是1,同1進行按位與運算後,將返回1,而1又會隱式轉換為true
  console.log("x為奇數");
}複製程式碼

按位或(|)

|不同於&, |運算子連線的兩個數, 只要其二進位制補碼形式的各位上有一個為1, 該位的運算就返回1, 否則返回0. 比如 3 和 12 進行按位或運算, 以下是運算過程:

    0011    //3的二進位制補碼形式
|    1100    //12的二進位制補碼形式
--------------------
    1111    //15, 相同位數依次運算,遇1返回1,故最終結果為4個1.複製程式碼

由上, 3|12的運算結果為15. 實際上, 由於按位與(&)運算同位上返回0的要求較為嚴苛, 因此, 它是一種趨向增大最小值的運算. 對於按位或(|)運算, 滿足如下規律:

  1. 數值與自身按位或運算返回數值自身.
  2. 2的整數次方的值與它的相對數按位或運算返回它的相對數.
  3. 任意整數與0進行按位或運算, 都將會返回它本身.
  4. 任意整數與-1進行按位或運算, 都將返回-1.
  5. 按位或運算的結果不小於兩數中的最小值.

稍微利用公式1, 我們便可以將非整數取整. 即 x|0 === Math.trunc(x) 如下:

console.log(5.2|0);//5
console.log(-5.2|0);//-5
console.log(Math.trunc(-5.2)===(-5.2|0));//true複製程式碼

為什麼 5.2|0 運算後會返回5呢? 這是因為浮點數並不支援位運算, 運算前, 5.2會轉換為整數5再和0進行位運算, 故, 最終返回5.

按位非(~)

~運算子, 返回數值二進位制補碼形式的反碼. 什麼意思呢, 就是說一個數值二進位制補碼形式中的每一位都將取反, 如果該位為1, 取反為0, 如果該位為0, 取反為1. 我們來舉個例子理解下:

~    0000 0000 0000 0000 0000 0000 0000 0011    //3的32位二進位制補碼形式
--------------------------------------------
    1111 1111 1111 1111 1111 1111 1111 1100    //按位取反後為負數(最高位(第一位)表示正負,1代表負,0代表正)
--------------------------------------------
    1000 0000 0000 0000 0000 0000 0000 0011    //負數的二進位制轉換為十進位制時,符號位不變,其它位取反(後+1)
    1000 0000 0000 0000 0000 0000 0000 0100 // +1
--------------------------------------------
                                      -4     //最終運算結果為-4複製程式碼

實際上, 按位非(~)操作不需要這麼興師動眾地去計算, 它有且僅有一條運算規律:

  • 按位非操作一個數值, 等同於這個數值加1然後符號改變. 即: ~x === -x-1.
~5 ==> -5-1 === -6;
~-2016 ==> 2016-1 === 2015;複製程式碼

由上述公式可推出: ~~x === -(-x-1)-1 === x. 由於位運算擯除小數部分的特性, 連續兩次按位非也可用於將非整數取整. 即, ~~x === Math.trunc(x) 如下:

console.log(~~5.2);//5
console.log(~~-5.2);//-5
console.log(Math.trunc(-5.2)===(~~-5.2));//true複製程式碼

按位非(~)運算子只能用來求數值的反碼, 並且還不能輸出反碼的二進位制字串. 我們來稍微擴充套件下, 使它變得更易用.

function waveExtend(item){
  var s = typeof item == 'number' && translateBinary(~item);
  return typeof s == 'string'?s:[].map.call(item,function(v){
    return v==='-'?v:v^1;
  }).join('').replace(/^-?/,function(m){return m==''?'-':''});
}
waveExtend(-8);//111 -8反碼,正數省略的位全部為0
waveExtend(12);//-0011 12的反碼,負數省略的位全部為1複製程式碼

實際上, 按位非(~)運算子要求其運算數為整型, 如果運算數不是整型, 它將和其他位運算子一樣嘗試將其轉換為32位整型, 如果無法轉換, 就返回NaN. 那麼~NaN等於多少呢?

console.log(~function(){alert(20);}());//先alert(20),然後輸出-1複製程式碼

以上語句意在列印一個自執行函式的按位非運算結果. 而該自執行函式又沒有顯式指定返回值, 預設將返回undefined. 因此它實際上是在輸出~undefined的值. 而undefined值不能轉換成整型, 通過測試, 運算結果為-1(即~NaN === -1). 我們不妨來看看下來測試, 以便加深理解.

console.log(~'abc');//-1
console.log(~[]);//-1
console.log(~{});//-1
console.log(~function(){});//-1
console.log(~/\d/);//-1
console.log(~Infinity);//-1
console.log(~null);//-1
console.log(~undefined);//-1
console.log(~NaN);//-1複製程式碼

按位異或(^)

^運算子連線的兩個數, 它們二進位制補碼形式的值每位參與運算, 只有相對應的每位值不同, 才返回1, 否則返回0.
(相同則消去, 有些類似兩兩消失的消消樂). 如下:

    0011    //3的二進位制補碼形式
^    1000    //8的二進位制補碼形式
--------------------
    1011    //11, 相同位數依次運算, 值不同的返回1複製程式碼

對於按位異或(^)操作, 滿足如下規律:

  1. 由於按位異或位運算的特殊性, 數值與自身按位異或運算返回0. 如: 8^8=0 , 公式為 a^a=0 .
  2. 任意整數與0進行按位異或運算, 都將會返回它本身. 如: 0^-98=-98 , 公式為 0^a=a.
  3. 任意整數x與1(2的0次方)進行按位異或運算, 若它為奇數, 則返回 x-1, 若它為偶數, 則返回 x+1 . 如: 1^-9=-10 , 1^100=101 . 公式為 1^奇=奇-1 , 1^偶=偶+1 ; 推而廣之, 任意整數x與2的n次方進行按位異或運算, 若它的二進位制補碼形式的倒數第n+1位是1, 則返回 x-2的n次方, 反之若為0, 則返回 x+2的n次方 .
  4. 任意整數x與-1(負2的1次方+1)進行按位異或運算, 則將返回 -x-1, 相當於~x運算 . 如: -1^100=-101 , -1^-9=8 . 公式為 -1^x=-x-1=~x .
  5. 任意整數連續按位異或兩次相同的數值, 返回它本身. 如: 3^8^8=3 , 公式為 a^b^b=aa^b^a=b .
  6. 按位異或滿足運算元與運算結果3個數值之間的交換律: 按位異或的兩個數值, 以及他們運算的結果, 共三個數值可以兩兩異或得到另外一個數值 . 如: 3^9=10 , 3^10=9 , 9^10=3 ; 公式為 a^b=c , a^c=b , b^c=a .

以上公式中, 1, 2, 3和4都是由按位異或運算特性推出的, 公式5可由公式1和2推出, 公式6可由公式5推出.

由於按位異或運算的這種可交換的性質, 我們可用它輔助交換兩個整數的值. 如下, 假設這兩個值為a和b:

var a=1,b=2;
//常規方法
var tmp = a;
a=b;
b=tmp;
console.log(a,b);//2 1

//使用按位異或~的方法
a=a^b;    //假設a,b的原始值分別為a0,b0
b=a^b;    //等價於 b=a0^b0^b0 ==> b=a0
a=a^b;    //等價於 a=a0^b0^a0 ==> a=b0
console.log(a,b);//2 1
//以上可簡寫為
a^=b;b^=a;a^=b;複製程式碼

位運算小結

由上可以看出:

  • 由於連線兩個數值的位運算均是對相同的位進行比較操作, 故運算數值的先後位置並不重要, 這些位運算(& | ^)滿足交換律. 即: a操作符b === b操作符a.
  • 位運算中, 數字0和1都比較特殊. 記住它們的規律, 常可簡化運算.
  • 位運算(&|~^)可用於取整, 同 Math.trunc().

有符號左移(<<)

<<運算子, 表示將數值的32位二進位制補碼形式的除符號位之外的其他位都往左移動若干位數. 當x為整數時, 有: x<<n === x*Math.pow(2,n) 如下:

console.log(1<<3);//8
console.log(100<<4);//1600複製程式碼

如此, Math.pow(2,n) 便可簡寫為 1<<n.

運算子之一為NaN

對於表示式 x<<n , 當運算數x無法被轉換為整數時,運算結果為0.

console.log({}<<3);//0
console.log(NaN<<2);//0複製程式碼

當運算數n無法被轉換為整數時,運算結果為x. 相當於 x<<0 .

console.log(2<<NaN);//2複製程式碼

當運算數x和n均無法被轉換為整數時,運算結果為0.

console.log(NaN<<NaN);//0複製程式碼

有符號右移(>>)

>>運算子, 除了方向向右, 其他同<<運算子. 當x為整數時, 有: x>>n === Math.floor(x*Math.pow(2,-n)) . 如下:

console.log(-5>>2);//-2
console.log(-7>>3);//-1複製程式碼

右移負整數時, 返回值最大為-1.

右移正整數時, 返回值最小為0.

其他規律請參考 有符號左移時運算子之一為NaN的場景.

無符號右移(>>>)

>>>運算子, 表示連同符號也一起右移.

注意:無符號右移(>>>)會把負數的二進位制碼當成正數的二進位制碼. 如下:

console.log(-8>>>5);//134217727
console.log(-1>>>0);//4294967295複製程式碼

以上, 雖然-1沒有發生向右位移, 但是-1的二進位制碼, 已經變成了正數的二進位制碼. 我們來回顧下這個過程.

translateAry(-1);//-1,補全-1的二進位制碼至32位: 11111111111111111111111111111111
translateAry('11111111111111111111111111111111');//4294967295複製程式碼

可見, -1的二進位制原碼本就是32個1, 將這32個1當正數的二進位制處理, 直接還原成十進位制, 剛好就是 4294967295.

由此, 使用 >>>運算子, 即使是右移0位, 對於負數而言也是翻天覆地的變化. 但是對於正數卻沒有改變. 利用這個特性, 可以判斷數值的正負. 如下:

function getSymbol(num){
  return num === (num>>>0)?"正數":"負數";
}
console.log(getSymbol(-100), getSymbol(123));//負數 正數複製程式碼

其他規律請參考 有符號左移時運算子之一為NaN的場景.

運算子優先順序

使用運算子, 如果不知道它們的運算優先順序. 就像駕駛法拉利卻分不清楚油門和剎車一樣恐怖. 因此我為您準備了常用運算子的運算優先順序表. 請對號入座.

優先順序 運算子 描述
1 後置++ , 後置-- , [] , () 或 . 後置++,後置--,陣列下標,括號 或 屬性選擇
2 - , 前置++ , 前置-- , ! 或 ~ 負號,前置++,前置--, 邏輯非 或 按位非
3 * , / 或 % 乘 , 除 或 取模
4 + 或 - 加 或 減
5 << 或 >> 左移 或 右移
6 > , >= , < 或 <= 大於, 大於等於, 小於 或 小於等於
7 == 或 != 等於 或 不等於
8 & 按位與
9 ^ 按位異或
10 按位或
11 && 邏輯與
12 邏輯或 邏輯或
13 ?: 條件運算子
14 =,/=,*=,%=,+=,-=,<<=,>>=,&=,^=,按位或後賦值 各種運算後賦值
15 , 逗號

可以看到, ① 除了按位非(~)以外, 其他的位運算子的優先順序都是低於+-運算子的; ② 按位與(&), 按位異或(^) 或 按位或(|) 的運算優先順序均低於比較運算子(>,<,=等); ③位運算子中按位或(|)優先順序最低.

綜合運用

計算絕對值

使用有符號右移(>>)運算子, 以及按位異或(^)運算子, 我們可以實現一個 Math.abs方法. 如下:

function abs(num){
  var x = num>>31,    //保留32二進位制中的符號位,根據num的正負性分別返回0或-1
      y = num^x;    //返回正數,且利用按位異或中的公式2,若num為正數,num^0則返回num本身;若num為負數,則相當於num^-1,利用公式4, 此時返回-num-1
  return y-x;        //若num為正數,則返回num-0即num;若num為負數則返回-num-1-(-1)即|num|
}複製程式碼

比較兩數是否符號相同

通常, 比較兩個數是否符號相同, 我們使用x*y>0 來判斷即可. 但如果利用按位異或(^), 運算速度將更快.

console.log(-17 ^ 9 > 0);//false複製程式碼

對2的n次方取模(n為正整數)

比如 123%8, 實際上就是求一個餘數, 並且這個餘數還不大於8, 最大為7. 然後剩下的就是比較二進位制值裡, 123與7有幾成相似了. 便不難推出公式: x%(1<<n)==x&(1<<n)-1 .

console.log(123%8);//3
console.log(123&(1<<3)-1);//3 , 為什麼-1時不用括號括起來, 這是因為-優先順序高於&複製程式碼

統計正數二進位制值中1的個數

不妨先判斷n的奇偶性, 為奇數時計數器增加1, 然後將n右移一位, 重複上面步驟, 直到遞迴退出.

function getTotalForOne(n){
      return n?(n&1)+arguments.callee(n>>1):0;
}
getTotalForOne(9);//2複製程式碼

實現加法運算

加法運算, 從二進位制值的角度看, 有 ①同位相加 和 ②遇2進1 兩種運算(實際上, 十進位制運算也是一樣, 同位相加, 遇10進1).

首先我們看看第①種, 同位相加, 不考慮②遇2進1.

1 + 1 = 0
1 + 0 = 1
0 + 1 = 1
0 + 0 = 0複製程式碼

以上運算過程有沒有很熟悉. 是不是和按位異或(^)運算有著驚人的相似. 如:

1 ^ 1 = 0
1 ^ 0 = 1
0 ^ 1 = 1
0 ^ 0 = 0複製程式碼

因此①同位相加的運算, 完全可由按位異或(^)代替, 即: x^y.

那麼②遇2進1 應該怎麼實現呢? 實際上, 非位移位運算中, 只有按位與(&)才能滿足遇2的場景, 且只有有符號左移(<<)能滿足進1的場景.

現在範圍縮小了, 就看&和<<運算子能不能真正滿足需要了. 值得高興的是, 按位與(&)只有在同位都是1的情況下才返回1, 其他情況均返回0. 如果對其運算結果再做左移一位的運算, 即: (x&y)<<1. 剛好滿足了②遇2進1的場景.

因為我們是將①同位相加和②遇2進1的兩種運算分開進行. 那麼最終的加法運算結果應該還要做一次加法. 如下:

最終公式: x + y = x^y + (x&y)<<1

這個公式並不完美, 因為它還是使用了加法, 推導公式怎麼能直接使用推導結果呢? 太可怕了, 就不怕掉入遞迴深淵嗎? 下面我們就來繞過這個坑. 而繞過這個坑有一個前提, 那就是隻要 x^y 或 (x&y)<<1中有一個值為0就行了, 這樣便不用進行加法運算了. 講了這麼多, 不如看程式碼.

function add(x, y){
  var _x = x^y,
      _y = (x&y)<<1;
  return !_x && _y || !_y && _x || arguments.callee(_x,_y);
}
add(12345678,87654321);//999999999
add(9527,-12);//9515複製程式碼

總結

最後補充一點: 位運算一般只適用 [-2^31, 2^31-1] (即 -2147483648~2147483647) 以內的正負數. 超過這個範圍, 計算將可能出現錯誤. 如下:

console.log(1<<31);//-2147483648複製程式碼

由於數值(2^31)超過了31位(加上保留的一個符號位,共32位), 故計算出錯, 於是按照負數的方式解釋二進位制的值了.說好的不改變符號呢!!!

本文囉嗦幾千字, 就為了說清楚兩個事兒. ① Math物件中, 比較常用的就是數值運算方法, 不妨多看看, 其他的知道有這個api就行了. ② 位運算中, 則需要基本瞭解每種位運算子的運算方式, 如果能注意運算中 0和1等特殊數值 的一些妙用就更好了. 無論如何, 本文不可能面面俱到. 如果您對負數的位運算不甚理解, 建議去補下計算機的補碼. 希望能對您有所幫助.

註解

  1. 相反數 : 只有符號不同的兩個數, 我們就說其中一個是另一個的相反數.
  2. 補碼: 在計算機系統中, 數值一律用補碼來表示和儲存, 且正數的原碼和補碼相同, 負數的補碼等於其原碼按位取反再加1.

本問就討論這麼多內容, 如果您有什麼問題或好的想法歡迎在下方參與留言和評論.

本文作者: louis

本文連結: louiszhai.github.io/2016/07/01/…

參考文章

相關文章