全面解讀Math對象及位運算

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

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

導讀

截至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以及與之息息相關的位運算, 一探究竟.java

爲何Math這麼設計

衆所周知, 若是須要使用js進行一些常規的數學運算, 是一件十分麻煩的事情. 爲了解決這個問題, ECMAScript 在1.1版本中便引入了 Math. Math 之因此被設計成一個對象, 而不是構造器, 是由於對象中的方法或屬性能夠做爲靜態方法或常量直接被調用, 方便使用, 同時, Math 也沒有建立實例的必要.git

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對象作了一些擴展, 增長了一系列便捷的方法. 而這些方法大體能夠分爲如下三類.github

三角函數

方法名 描述
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方法的一些規律

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

  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代碼通過解釋編譯, 最終會以二進制的方式進行運算. 這種運算方式效率較低, 那麼能不能進一步提升運算的效率的呢? 若是咱們使用位運算就可. 這是由於位運算本就是直接進行二進制運算.app

數值的二進制值

因爲位運算是基於二進制的, 所以咱們須要先獲取數值的二進制值. 實際上, toString 方法已經幫咱們作好了一部分工做, 以下:dom

//正整數可經過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/…

參考文章

相關文章
相關標籤/搜索