全面解讀Math對象及位運算

<p>本文轉載自:<a href="https://segmentfault.com/a/1190000008786789" title="全面解讀Math對象及位運算">全面解讀Math對象及位運算</a></p>
<p>Math方法和位運算幾乎是被忽略得最嚴重的知識點, 和正則一樣, 不用不知道, 一用到處查. 爲了告別這種低效的編程模式, 我特地總結此篇, 系統梳理了這兩個知識點. 以此爲冊, 助你攻破它們.</p>
<p>原文: <a href="http://louiszhai.github.io/2016/07/01/Math/">全面解讀Math對象及位運算</a></p>
<h3>導讀</h3>
<p>截至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以及與之息息相關的位運算, 一探究竟.</p>
<h3>爲什麼Math這麼設計</h3>
<p>衆所周知, 如果需要使用js進行一些常規的數學運算, 是一件十分麻煩的事情. 爲了解決這個問題, ECMAScript 1.1版本中便引入了 Math. Math 之所以被設計成一個對象, 而不是構造器, 是因爲對象中的方法或屬性可以作爲靜態方法或常量直接被調用, 方便使用, 同時, Math 也沒有創建實例的必要.</p>
<h3>Math中的屬性</h3>
<table>
<thead><tr>
<th align="center">屬性名</th>
<th align="center">描述</th>
<th align="center"></th>
</tr></thead>
<tbody>
<tr>
<td align="center">Math.E</td>
<td align="center">歐拉常數,也是自然對數的底數</td>
<td align="center">2.718</td>
</tr>
<tr>
<td align="center">Math.LN2</td>
<td align="center">2的自然對數</td>
<td align="center">0.693</td>
</tr>
<tr>
<td align="center">Math.LN10</td>
<td align="center">10的自然對數</td>
<td align="center">2.303</td>
</tr>
<tr>
<td align="center">Math.LOG2E</td>
<td align="center">2爲底E的對數</td>
<td align="center">1.443</td>
</tr>
<tr>
<td align="center">Math.LOG10E</td>
<td align="center">10爲底E的對數</td>
<td align="center">0.434</td>
</tr>
<tr>
<td align="center">Math.PI</td>
<td align="center">圓周率</td>
<td align="center">3.14</td>
</tr>
<tr>
<td align="center">Math.SQRT1_2</td>
<td align="center">1/2的平方根</td>
<td align="center">0.707</td>
</tr>
<tr>
<td align="center">Math.SQRT2</td>
<td align="center">2的平方根</td>
<td align="center">1.414</td>
</tr>
</tbody>
</table>
<h3>Math中的方法</h3>
<p>Math對象本就有很多用於運算的方法, 值得關注的是, ES6 規範又對Math對象做了一些擴展, 增加了一系列便捷的方法. 而這些方法大致可以分爲以下三類.</p>
<h3>三角函數</h3>
<table>
<thead><tr>
<th align="center">方法名</th>
<th align="center">描述</th>
</tr></thead>
<tbody>
<tr>
<td align="center">Math.sin(x)</td>
<td align="center">返回x的正弦值</td>
</tr>
<tr>
<td align="center">Math.sinh(x) ES6新增</td>
<td align="center">返回x的雙曲正弦值</td>
</tr>
<tr>
<td align="center">Math.cos(x)</td>
<td align="center">返回x的餘弦值</td>
</tr>
<tr>
<td align="center">Math.cosh(x) ES6新增</td>
<td align="center">返回x的雙曲餘弦值</td>
</tr>
<tr>
<td align="center">Math.tan(x)</td>
<td align="center">返回x的正切值</td>
</tr>
<tr>
<td align="center">Math.tanh(x) ES6新增</td>
<td align="center">返回x的雙曲正切值</td>
</tr>
<tr>
<td align="center">Math.asin(x)</td>
<td align="center">返回x的反正弦值</td>
</tr>
<tr>
<td align="center">Math.asinh(x) ES6新增</td>
<td align="center">返回x的反雙曲正弦值</td>
</tr>
<tr>
<td align="center">Math.acos(x)</td>
<td align="center">返回x的反餘弦值</td>
</tr>
<tr>
<td align="center">Math.atan(x)</td>
<td align="center">返回x的反正切值</td>
</tr>
<tr>
<td align="center">Math.atan2(x, y)</td>
<td align="center">返回 y/x 的反正切值</td>
</tr>
<tr>
<td align="center">Math.atanh(x) ES6新增</td>
<td align="center">返回 x 的反雙曲正切值</td>
</tr>
</tbody>
</table>
<h3>數學運算方法</h3>
<table>
<thead><tr>
<th align="center">方法名</th>
<th align="center">描述</th>
<th align="center">例子</th>
</tr></thead>
<tbody>
<tr>
<td align="center">Math.sqrt(x)</td>
<td align="center">返回x的平方根</td>
<td align="center">Math.sqrt(9);//3</td>
</tr>
<tr>
<td align="center">Math.exp(x)</td>
<td align="center">返回歐拉常數(e)x次冪</td>
<td align="center">Math.exp(1);//2.718</td>
</tr>
<tr>
<td align="center">Math.pow(x,y)</td>
<td align="center">返回xy次冪, 如果y未初始化, 則返回x</td>
<td align="center">Math.pow(2, 3);//8</td>
</tr>
<tr>
<td align="center">Math.expm1(x) ES6新增</td>
<td align="center">返回歐拉常數(e)x次冪減去1的值</td>
<td align="center">Math.exp(1);//1.718</td>
</tr>
<tr>
<td align="center">Math.log(x)</td>
<td align="center">返回x的自然對數</td>
<td align="center">Math.log(1);//0</td>
</tr>
<tr>
<td align="center">Math.log1p(x) ES6新增</td>
<td align="center">返回x+1後的自然對數</td>
<td align="center">Math.log1p(0);//0</td>
</tr>
<tr>
<td align="center">Math.log2(x) ES6新增</td>
<td align="center">返回x2爲底的對數</td>
<td align="center">Math.log2(8);//3</td>
</tr>
<tr>
<td align="center">Math.log10(x) ES6新增</td>
<td align="center">返回x10爲底的對數</td>
<td align="center">Math.log10(100);//2</td>
</tr>
<tr>
<td align="center">Math.cbrt(x) ES6新增</td>
<td align="center">返回x的立方根</td>
<td align="center">Math.cbrt(8);//2</td>
</tr>
<tr>
<td align="center">Math.clz32()  ES6新增</td>
<td align="center">返回一個數字在轉換成 32位無符號整型數字的二進制形式後, 開頭的 0 的個數</td>
<td align="center">Math.clz32(2);//30</td>
</tr>
<tr>
<td align="center">Math.hypot(x,y,z) ES6新增</td>
<td align="center">返回所有參數的平方和的平方根</td>
<td align="center">Math.hypot(3,4);//5</td>
</tr>
<tr>
<td align="center">Math.imul(x,y) ES6新增</td>
<td align="center">返回兩個參數的類C32位整數乘法運算的運算結果</td>
<td align="center">Math.imul(0xffffffff, 5);//-5</td>
</tr>
</tbody>
</table>
<h3>數值運算方法</h3>
<table>
<thead><tr>
<th align="center">方法名</th>
<th align="center">描述</th>
<th align="center">例子</th>
</tr></thead>
<tbody>
<tr>
<td align="center">Math.abs(x)</td>
<td align="center">返回x的絕對值</td>
<td align="center">Math.abs(-5);//5</td>
</tr>
<tr>
<td align="center">Math.floor(x)</td>
<td align="center">返回小於x的最大整數</td>
<td align="center">Math.floor(8.2);//8</td>
</tr>
<tr>
<td align="center">Math.ceil(x)</td>
<td align="center">返回大於x的最小整數</td>
<td align="center">Math.ceil(8.2);//9</td>
</tr>
<tr>
<td align="center">Math.trunc(x) ES6新增</td>
<td align="center">返回x的整數部分</td>
<td align="center">Math.trunc(1.23);//1</td>
</tr>
<tr>
<td align="center">Math.fround(x) ES6新增</td>
<td align="center">返回離它最近的<a href="https://en.wikipedia.org/wiki/Single-precision_floating-point_format">單精度浮點數</a>形式的數字</td>
<td align="center">Math.fround(1.1);//1.100000023841858</td>
</tr>
<tr>
<td align="center">Math.min(x,y,z)</td>
<td align="center">返回多個數中的最小值</td>
<td align="center">Math.min(3,1,5);//1</td>
</tr>
<tr>
<td align="center">Math.max(x,y,z)</td>
<td align="center">返回多個數中的最大值</td>
<td align="center">Math.max(3,1,5);//5</td>
</tr>
<tr>
<td align="center">Math.round(x)</td>
<td align="center">返回四捨五入後的整數</td>
<td align="center">Math.round(8.2);//8</td>
</tr>
<tr>
<td align="center">Math.random()</td>
<td align="center">返回01之間的僞隨機數</td>
<td align="center">Math.random();</td>
</tr>
<tr>
<td align="center">Math.sign(x) ES6新增</td>
<td align="center">返回一個數的符號( 5種返回值, 分別是 1, -1, 0, -0, NaN. 代表的各是正數, 負數, 正零, 負零, NaN)</td>
<td align="center">Math.sign(-5);//-1</td>
</tr>
</tbody>
</table>
<h4>:Number類型的數值運算方法</h4>
<p>Number.prototype中有一個方法叫做toFixed(), 用於將數值裝換爲指定小數位數的形式.</p>
<ul>
<li><p>沒有參數或者參數爲零的情況下, toFixed() 方法返回該數值的四捨五入後的整數形式, 等同於 Math.round(x);</p></li>
<li><p>其他情況下, 返回該數的指定小數位數的四捨五入後的結果.</p></li>
</ul>
<pre class="line-numbers"><code class="language-javascript">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</code></pre>
<h3>Math方法的一些規律</h3>
<p>以上, 數值運算中, 存在如下規律:</p>
<ol>
<li><p>Math.trunc(x) 方法當 ① x爲正數時, 運算結果同 Math.floor(x); ② x爲負數時, 運算結果同 Math.ceil(x). 實際上, 它完全可以由位運算替代, 且運算速度更快, 2.5&amp;-1 2.5|0 <del>2.5 2.5^0 , 它們的運算結果都爲2; -2.5&amp;-1 -2.5|0 </del>-2.5 -2.5^0 , 它們的運算結果都爲-2;</p></li>
<li><p>Math.min(x,y,z) Math.max(x,y,z) 方法由於可接無限個參數, 可用於求數組元素的最小最大值. : <code class="language-javascript">Math.max.apply(null,[5,3,8,9]); // 9</code> . 但是Math.min 不傳參數返回 <code class="language-javascript">Infinity</code>, Math.max 不傳參數返回 <code class="language-javascript">-Infinity</code> .</p></li>
<li><p>稍微利用 Math.random() 方法的特性, 就可以生成任意範圍的數字. : 生成1080之間的隨機數, ~~(Math.random()*70 + 10);// 返回10~80之間的隨機數, 包含10不包含80</p></li>
</ol>
<p>除去上述方法, Math作爲對象, 繼承了來之Object對象的方法. 其中一些如下:</p>
<pre class="line-numbers"><code class="language-javascript">Math.valueOf();//返回Math對象本身
+Math; //NaN, 試圖轉換成數字,由於不能轉換爲數字,返回NaN
Math.toString();//"[object Math]"</code></pre>
<h3>位運算</h3>
<p>Math對象提供的方法種類繁多, 且覆蓋面非常全面, 基本上能夠滿足日常開發所需. 但同時我們也都知道, 使用Math對象的方法進行數值運算時, js代碼經過解釋編譯, 最終會以二進制的方式進行運算. 這種運算方式效率較低, 那麼能不能進一步提高運算的效率的呢? 如果我們使用位運算就可. 這是因爲位運算本就是直接進行二進制運算.</p>
<h3>數值的二進制值</h3>
<p>由於位運算是基於二進制的, 因此我們需要先獲取數值的二進制值. 實際上, toString 方法已經幫我們做好了一部分工作, 如下:</p>
<pre class="line-numbers"><code class="language-javascript">//正整數可通過toString獲取
12..toString(2);//1100
//負整數問題就來了
(-12).toString(2);//-1100</code></pre>
<p>已知: 負數在計算機內部是採用補碼錶示的. 例如 -1, 1的原碼是 0000 0001, 那麼1的反碼是 1111 1110, 補碼是 1111 1111.</p>
<p>: 負數的十進制轉換爲二進制時,符號位不變,其它位取反後+1. : <strong><code class="language-javascript">-x的二進制 = x的二進制取反+1</code></strong> . 由按位取反可藉助^運算符, 故負整數的二進制可以藉助下面這個函數來獲取:</p>
<pre class="line-numbers"><code class="language-javascript">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</code></pre>
<p>然後, 多試幾次就會發現:</p>
<pre class="line-numbers"><code class="language-javascript">getBinary(-1) == 1..toString(2); //true
getBinary(-2) == 2..toString(2); //true
getBinary(-4) == 4..toString(2); //true
getBinary(-8) == 8..toString(2); //true</code></pre>
<p>這表明:</p>
<ul><li><p><strong>2的整數次方的值與它的相對數, 他們後面真正有效的那幾位都相同</strong>.</p></li></ul>
<p>同樣, 負數的二進制轉十進制時, 符號位不變, 其他位取反後+1. 可參考:</p>
<pre class="line-numbers"><code class="language-javascript">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</code></pre>
<p>由上, 二進制轉十進制和十進制轉二進制的函數, 大部分都可以共用, 因此下面提供一個統一的函數解決它們的互轉問題:</p>
<pre class="line-numbers"><code class="language-javascript">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</code></pre>
<h4>常用的二進制數</h4>
<table>
<thead><tr>
<th align="center">二進制數</th>
<th align="center">二進制值</th>
</tr></thead>
<tbody>
<tr>
<td align="center">0xAAAAAAAA</td>
<td align="center">10101010101010101010101010101010</td>
</tr>
<tr>
<td align="center">0x55555555</td>
<td align="center">01010101010101010101010101010101</td>
</tr>
<tr>
<td align="center">0xCCCCCCCC</td>
<td align="center">11001100110011001100110011001100</td>
</tr>
<tr>
<td align="center">0x33333333</td>
<td align="center">00110011001100110011001100110011</td>
</tr>
<tr>
<td align="center">0xF0F0F0F0</td>
<td align="center">11110000111100001111000011110000</td>
</tr>
<tr>
<td align="center">0x0F0F0F0F</td>
<td align="center">00001111000011110000111100001111</td>
</tr>
<tr>
<td align="center">0xFF00FF00</td>
<td align="center">11111111000000001111111100000000</td>
</tr>
<tr>
<td align="center">0x00FF00FF</td>
<td align="center">00000000111111110000000011111111</td>
</tr>
<tr>
<td align="center">0xFFFF0000</td>
<td align="center">11111111111111110000000000000000</td>
</tr>
<tr>
<td align="center">0x0000FFFF</td>
<td align="center">00000000000000001111111111111111</td>
</tr>
</tbody>
</table>
<p>現在也可以使用上述方法來驗證下常用的二進制值對不對. 如下:</p>
<pre class="line-numbers"><code class="language-javascript">translateBinary(0xAAAAAAAA);//"10101010101010101010101010101010"</code></pre>
<h3>按位與(&amp;)</h3>
<p>&amp;運算符用於連接兩個數, 連接的兩個數它們二進制補碼形式的值每位都將參與運算, 只有相對應的位上都爲1, 該位的運算才返回1. 比如 3 9 進行按位與運算, 以下是運算過程:</p>
<pre class="line-numbers"><code class="language-javascript">    0011    //3的二進制補碼形式
&amp;    1001    //9的二進制補碼形式
--------------------
0001    //1,相同位數依次運算,除最後一位都是1,返回1以外, 其它位數由於不同時爲1都返回0</code></pre>
<p>由上, 3&amp;9的運算結果爲1. 實際上, 由於按位與(&amp;)運算同位上返回1的要求較爲嚴苛, 因此, 它是一種趨向減小最大值的運算.(無論最大值是正數還是負數, 參與按位與運算後, 該數總是趨向減少二進制值位上1的數量, 因此總是有值減小的趨勢. ) 對於按位與(&amp;)運算, 滿足如下規律:</p>
<ol>
<li><p><strong>數值與自身(或者-1)按位與運算返回數值自身</strong>.</p></li>
<li><p><strong>2的整數次方的值與它的相對數按位與運算返回它自身</strong>.</p></li>
<li><p><strong>任意整數與0進行按位與運算, 都將會返回0</strong>.</p></li>
<li><p><strong>任意整數與1進行按位與運算, 都只有0 1 兩個返回值</strong>.</p></li>
<li><p><strong>按位與運算的結果不大於兩數中的最大值</strong>.</p></li>
</ol>
<p>由公式1, 我們可以對非整數取整. <code class="language-javascript">x&amp;x === x&amp;-1 === Math.trunc(x)</code> 如下:</p>
<pre class="line-numbers"><code class="language-javascript">console.log(5.2&amp;5.2);//5
console.log(-5.2&amp;-1);//-5
console.log(Math.trunc(-5.2)===(-5.2&amp;-1));//true</code></pre>
<p>由公式4, 我們可以由此判斷數值是否爲奇數. 如下:</p>
<pre class="line-numbers"><code class="language-javascript">if(1 &amp; x){//如果x爲奇數,它的二進制補碼形式最後一位必然是1,1進行按位與運算後,將返回1,1又會隱式轉換爲true
   console.log("x爲奇數");
}</code></pre>
<h3>按位或(|)</h3>
<p>|不同於&amp;, |運算符連接的兩個數, 只要其二進制補碼形式的各位上有一個爲1, 該位的運算就返回1, 否則返回0. 比如 3 12 進行按位或運算, 以下是運算過程:</p>
<pre class="line-numbers"><code class="language-javascript">    0011    //3的二進制補碼形式
|    1100    //12的二進制補碼形式
--------------------
1111    //15, 相同位數依次運算,1返回1,故最終結果爲41.</code></pre>
<p>由上, 3|12的運算結果爲15. 實際上, 由於按位與(&amp;)運算同位上返回0的要求較爲嚴苛, 因此, 它是一種趨向增大最小值的運算. 對於按位或(|)運算, 滿足如下規律:</p>
<ol>
<li><p><strong>數值與自身按位或運算返回數值自身</strong>.</p></li>
<li><p><strong>2的整數次方的值與它的相對數按位或運算返回它的相對數</strong>.</p></li>
<li><p><strong>任意整數與0進行按位或運算, 都將會返回它本身</strong>.</p></li>
<li><p><strong>任意整數與-1進行按位或運算, 都將返回-1</strong>.</p></li>
<li><p><strong>按位或運算的結果不小於兩數中的最小值</strong>.</p></li>
</ol>
<p>稍微利用公式1, 我們便可以將非整數取整.   <strong><code class="language-javascript">x|0 === Math.trunc(x)</code></strong> 如下:</p>
<pre class="line-numbers"><code class="language-javascript">console.log(5.2|0);//5
console.log(-5.2|0);//-5
console.log(Math.trunc(-5.2)===(-5.2|0));//true</code></pre>
<p>爲什麼 5.2|0 運算後會返回5? 這是因爲浮點數並不支持位運算, 運算前, 5.2會轉換爲整數5再和0進行位運算, , 最終返回5.</p>
<h3>按位非(~)</h3>
<p>~運算符, 返回數值二進制補碼形式的反碼. 什麼意思呢, 就是說一個數值二進制補碼形式中的每一位都將取反, 如果該位爲1, 取反爲0, 如果該位爲0, 取反爲1. 我們來舉個例子理解下:</p>
<pre class="line-numbers"><code class="language-javascript">~    0000 0000 0000 0000 0000 0000 0000 0011    //332位二進制補碼形式
--------------------------------------------
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</code></pre>
<p>實際上, 按位非(~)操作不需要這麼興師動衆地去計算, 它有且僅有一條運算規律:</p>
<ul><li><p><strong>按位非操作一個數值, 等同於這個數值加1然後符號改變. : <code class="language-javascript">~x === -x-1</code></strong>.</p></li></ul>
<pre class="line-numbers"><code class="language-javascript">~5 ==&gt; -5-1 === -6;
~-2016 ==&gt; 2016-1 === 2015;</code></pre>
<p>由上述公式可推出: <strong><code class="language-javascript">~~x === -(-x-1)-1 === x</code></strong>. 由於位運算擯除小數部分的特性, 連續兩次按位非也可用於將非整數取整. , <strong><code class="language-javascript">~~x === Math.trunc(x)</code></strong> 如下:</p>
<pre class="line-numbers"><code class="language-javascript">console.log(~~5.2);//5
console.log(~~-5.2);//-5
console.log(Math.trunc(-5.2)===(~~-5.2));//true</code></pre>
<p>按位非(~)運算符只能用來求數值的反碼, 並且還不能輸出反碼的二進制字符串. 我們來稍微擴展下, 使它變得更易用.</p>
<pre class="line-numbers"><code class="language-javascript">function waveExtend(item){
   var s = typeof item == 'number' &amp;&amp; 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</code></pre>
<p>實際上, 按位非(~)運算符要求其運算數爲整型, 如果運算數不是整型, 它將和其他位運算符一樣嘗試將其轉換爲32位整型, 如果無法轉換, 就返回NaN. 那麼~NaN等於多少呢?</p>
<pre class="line-numbers"><code class="language-javascript">console.log(~function(){alert(20);}());//alert(20),然後輸出-1</code></pre>
<p>以上語句意在打印一個自執行函數的按位非運算結果. 而該自執行函數又沒有顯式指定返回值, 默認將返回undefined. 因此它實際上是在輸出~undefined的值. undefined值不能轉換成整型, 通過測試, 運算結果爲-1(~NaN === -1). 我們不妨來看看下來測試, 以便加深理解.</p>
<pre class="line-numbers"><code class="language-javascript">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</code></pre>
<h3>按位異或(^)</h3>
<p>^運算符連接的兩個數, 它們二進制補碼形式的值每位參與運算, 只有相對應的每位值不同, 才返回1, 否則返回0. <br>(相同則消去, 有些類似兩兩消失的消消樂). 如下:</p>
<pre class="line-numbers"><code class="language-javascript">    0011    //3的二進制補碼形式
^    1000    //8的二進制補碼形式
--------------------
1011    //11, 相同位數依次運算, 值不同的返回1</code></pre>
<p>對於按位異或(^)操作, 滿足如下規律:</p>
<ol>
<li><p><strong>由於按位異或位運算的特殊性, 數值與自身按位異或運算返回0</strong>. : <code class="language-javascript">8^8=0</code> , 公式爲 <code class="language-javascript">a^a=0</code> .</p></li>
<li><p><strong>任意整數與0進行按位異或運算, 都將會返回它本身</strong>. : <code class="language-javascript">0^-98=-98</code> , 公式爲 <code class="language-javascript">0^a=a</code>.</p></li>
<li><p><strong>任意整數x1(20次方)進行按位異或運算, 若它爲奇數, 則返回 <code class="language-javascript">x-1</code>, 若它爲偶數, 則返回 <code class="language-javascript">x+1</code> </strong>. : <code class="language-javascript">1^-9=-10</code> , <code class="language-javascript">1^100=101</code>  . 公式爲 <code class="language-javascript">1^=-1</code> , <code class="language-javascript">1^=+1</code> ; 推而廣之, <strong>任意整數x2n次方進行按位異或運算, 若它的二進制補碼形式的倒數第n+1位是1, 則返回 <code class="language-javascript">x-2n次方</code>, 反之若爲0, 則返回 <code class="language-javascript">x+2n次方</code> </strong>.</p></li>
<li><p><strong>任意整數x-1(21次方+1)進行按位異或運算, 則將返回 <code class="language-javascript">-x-1</code>, 相當於~x運算 </strong>. : <code class="language-javascript">-1^100=-101</code> , <code class="language-javascript">-1^-9=8</code> . 公式爲 <code class="language-javascript">-1^x=-x-1=~x</code> .</p></li>
<li><p><strong>任意整數連續按位異或兩次相同的數值, 返回它本身</strong>. : <code class="language-javascript">3^8^8=3</code> , 公式爲 <code class="language-javascript">a^b^b=a</code> <code class="language-javascript">a^b^a=b</code> .</p></li>
<li><p><strong>按位異或滿足操作數與運算結果3個數值之間的交換律: 按位異或的兩個數值, 以及他們運算的結果, 共三個數值可以兩兩異或得到另外一個數值</strong> . : <code class="language-javascript">3^9=10</code> , <code class="language-javascript">3^10=9</code> , <code class="language-javascript">9^10=3</code> ; 公式爲 <code class="language-javascript">a^b=c</code> , <code class="language-javascript">a^c=b</code> , <code class="language-javascript">b^c=a</code> .</p></li>
</ol>
<p>以上公式中, 1, 2, 34都是由按位異或運算特性推出的, 公式5可由公式12推出, 公式6可由公式5推出.</p>
<p>由於按位異或運算的這種可交換的性質, 我們可用它輔助交換兩個整數的值. 如下, 假設這兩個值爲ab:</p>
<pre class="line-numbers"><code class="language-javascript">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 ==&gt; b=a0
a=a^b;    //等價於 a=a0^b0^a0 ==&gt; a=b0
console.log(a,b);//2 1
//以上可簡寫爲
a^=b;b^=a;a^=b;</code></pre>
<h3>位運算小結</h3>
<p>由上可以看出:</p>
<ul>
<li><p>由於連接兩個數值的位運算均是對相同的位進行比較操作, 故運算數值的先後位置並不重要, 這些位運算(&amp; | ^)滿足交換律. : <code class="language-javascript">a操作符b === b操作符a</code>.</p></li>
<li><p>位運算中, 數字01都比較特殊. 記住它們的規律, 常可簡化運算.</p></li>
<li><p>位運算(&amp;|~^)可用於取整, Math.trunc().</p></li>
</ul>
<h3>有符號左移(&lt;&lt;)</h3>
<p>&lt;&lt;運算符, 表示將數值的32位二進制補碼形式的除符號位之外的其他位都往左移動若干位數. x爲整數時, : <code class="language-javascript">x&lt;&lt;n === x*Math.pow(2,n)</code> 如下:</p>
<pre class="line-numbers"><code class="language-javascript">console.log(1&lt;&lt;3);//8
console.log(100&lt;&lt;4);//1600</code></pre>
<p>如此, Math.pow(2,n) 便可簡寫爲 1&lt;&lt;n.</p>
<h4>運算符之一爲NaN</h4>
<p>對於表達式 <code class="language-javascript">x&lt;&lt;n</code> , <strong>當運算數x無法被轉換爲整數時,運算結果爲0.</strong></p>
<pre class="line-numbers"><code class="language-javascript">console.log({}&lt;&lt;3);//0
console.log(NaN&lt;&lt;2);//0</code></pre>
<p><strong>當運算數n無法被轉換爲整數時,運算結果爲x.</strong> 相當於 <code class="language-javascript">x&lt;&lt;0</code> .</p>
<pre class="line-numbers"><code class="language-javascript">console.log(2&lt;&lt;NaN);//2</code></pre>
<p><strong>當運算數xn均無法被轉換爲整數時,運算結果爲0.</strong></p>
<pre class="line-numbers"><code class="language-javascript">console.log(NaN&lt;&lt;NaN);//0</code></pre>
<h3>有符號右移(&gt;&gt;)</h3>
<p>&gt;&gt;運算符, 除了方向向右, 其他同&lt;&lt;運算符. x爲整數時, : <code class="language-javascript">x&gt;&gt;n === x*Math.pow(2,-n)</code> . 如下:</p>
<pre class="line-numbers"><code class="language-javascript">console.log(-5&gt;&gt;2);//-2
console.log(-7&gt;&gt;3);//-1</code></pre>
<p>右移負整數時, 返回值最大爲-1.</p>
<p>右移正整數時, 返回值最小爲0.</p>
<p>其他規律請參考 有符號左移時運算符之一爲NaN的場景.</p>
<h4>無符號右移(&gt;&gt;&gt;)</h4>
<p>&gt;&gt;&gt;運算符, 表示連同符號也一起右移. </p>
<p>注意:無符號右移(&gt;&gt;&gt;)會把負數的二進制碼當成正數的二進制碼. 如下:</p>
<pre class="line-numbers"><code class="language-javascript">console.log(-8&gt;&gt;&gt;5);//134217727
console.log(-1&gt;&gt;&gt;0);//4294967295</code></pre>
<p>以上, 雖然-1沒有發生向右位移, 但是-1的二進制碼, 已經變成了正數的二進制碼. 我們來回顧下這個過程.</p>
<pre class="line-numbers"><code class="language-javascript">translateAry(-1);//-1,補全-1的二進制碼至32: 11111111111111111111111111111111
translateAry('11111111111111111111111111111111');//4294967295</code></pre>
<p>可見, -1的二進制原碼本就是321, 將這321當正數的二進制處理, 直接還原成十進制, 剛好就是 4294967295.</p>
<p>由此, 使用 &gt;&gt;&gt;運算符, 即使是右移0, 對於負數而言也是翻天覆地的變化. 但是對於正數卻沒有改變. 利用這個特性, 可以判斷數值的正負. 如下:</p>
<pre class="line-numbers"><code class="language-javascript">function getSymbol(num){
   return num === (num&gt;&gt;&gt;0)?"正數":"負數";
}
console.log(getSymbol(-100), getSymbol(123));//負數 正數</code></pre>
<p>其他規律請參考 有符號左移時運算符之一爲NaN的場景.</p>
<h3>運算符優先級</h3>
<p>使用運算符, 如果不知道它們的運算優先級. 就像駕駛法拉利卻分不清楚油門和剎車一樣恐怖. 因此我爲您準備了常用運算符的運算優先級表. 請對號入座.</p>
<table>
<thead><tr>
<th align="center">優先級</th>
<th align="center">運算符</th>
<th align="center">描述</th>
</tr></thead>
<tbody>
<tr>
<td align="center">1</td>
<td align="center">後置++ , 後置-- , [] , () .</td>
<td align="center">後置++,後置--,數組下標,括號 或 屬性選擇</td>
</tr>
<tr>
<td align="center">2</td>
<td align="center">- , 前置++ , 前置-- , ! ~</td>
<td align="center">負號,前置++,前置--, 邏輯非 或 按位非</td>
</tr>
<tr>
<td align="center">3</td>
<td align="center">* , / %</td>
<td align="center">, 除 或 取模</td>
</tr>
<tr>
<td align="center">4</td>
<td align="center">+ -</td>
<td align="center">加 或 減</td>
</tr>
<tr>
<td align="center">5</td>
<td align="center">&lt;&lt; &gt;&gt;</td>
<td align="center">左移 或 右移</td>
</tr>
<tr>
<td align="center">6</td>
<td align="center">&gt; , &gt;= , &lt; &lt;=</td>
<td align="center">大於, 大於等於, 小於 或 小於等於</td>
</tr>
<tr>
<td align="center">7</td>
<td align="center">== !=</td>
<td align="center">等於 或 不等於</td>
</tr>
<tr>
<td align="center">8</td>
<td align="center">&amp;</td>
<td align="center">按位與</td>
</tr>
<tr>
<td align="center">9</td>
<td align="center">^</td>
<td align="center">按位異或</td>
</tr>
<tr>
<td align="center">10</td>
<td align="center"></td>
<td align="center">按位或</td>
</tr>
<tr>
<td align="center">11</td>
<td align="center">&amp;&amp;</td>
<td align="center">邏輯與</td>
</tr>
<tr>
<td align="center">12</td>
<td align="center">邏輯或</td>
<td align="center">邏輯或</td>
</tr>
<tr>
<td align="center">13</td>
<td align="center">?:</td>
<td align="center">條件運算符</td>
</tr>
<tr>
<td align="center">14</td>
<td align="center">=,/=,*=,%=,+=,-=,&lt;&lt;=,&gt;&gt;=,&amp;=,^=,按位或後賦值</td>
<td align="center">各種運算後賦值</td>
</tr>
<tr>
<td align="center">15</td>
<td align="center">,</td>
<td align="center">逗號</td>
</tr>
</tbody>
</table>
<p>可以看到, ① 除了按位非(~)以外, 其他的位運算符的優先級都是低於+-運算符的; ② 按位與(&amp;), 按位異或(^) 或 按位或(|) 的運算優先級均低於比較運算符(&gt;,&lt;,=); ③位運算符中按位或(|)優先級最低.</p>
<h3>綜合運用</h3>
<h3>計算絕對值</h3>
<p>使用有符號右移(&gt;&gt;)運算符, 以及按位異或(^)運算符, 我們可以實現一個 Math.abs方法. 如下:</p>
<pre class="line-numbers"><code class="language-javascript">function abs(num){
   var x = num&gt;&gt;31,    //保留32二進制中的符號位,根據num的正負性分別返回0-1
   y = num^x;    //返回正數,且利用按位異或中的公式2,num爲正數,num^0則返回num本身;num爲負數,則相當於num^-1,利用公式4, 此時返回-num-1
   return y-x;        //num爲正數,則返回num-0num;num爲負數則返回-num-1-(-1)|num|
}</code></pre>
<h3>比較兩數是否符號相同</h3>
<p>通常, 比較兩個數是否符號相同, 我們使用x*y&gt;0 來判斷即可. 但如果利用按位異或(^), 運算速度將更快.</p>
<pre class="line-numbers"><code class="language-javascript">console.log(-17 ^ 9 &gt; 0);//false</code></pre>
<h3>2n次方取模(n爲正整數)</h3>
<p>比如 123%8, 實際上就是求一個餘數, 並且這個餘數還不大於8, 最大爲7. 然後剩下的就是比較二進制值裏, 1237有幾成相似了. 便不難推出公式: <code class="language-javascript">x%(1&lt;&lt;n)==x&amp;(1&lt;&lt;n)-1</code> .</p>
<pre class="line-numbers"><code class="language-javascript">console.log(123%8);//3
console.log(123&amp;(1&lt;&lt;3)-1);//3 , 爲什麼-1時不用括號括起來, 這是因爲-優先級高於&amp;</code></pre>
<h3>統計正數二進制值中1的個數</h3>
<p>不妨先判斷n的奇偶性, 爲奇數時計數器增加1, 然後將n右移一位, 重複上面步驟, 直到遞歸退出.</p>
<pre class="line-numbers"><code class="language-javascript">function getTotalForOne(n){
   return n?(n&amp;1)+arguments.callee(n&gt;&gt;1):0;
}
getTotalForOne(9);//2</code></pre>
<h3>實現加法運算</h3>
<p>加法運算, 從二進制值的角度看, 同位相加 和 21 兩種運算(實際上, 十進制運算也是一樣, 同位相加, 101). </p>
<p>首先我們看看第, 同位相加, 不考慮21.</p>
<pre class="line-numbers"><code class="language-javascript">1 + 1 = 0
1 + 0 = 1
0 + 1 = 1
0 + 0 = 0</code></pre>
<p>以上運算過程有沒有很熟悉. 是不是和按位異或(^)運算有着驚人的相似. :</p>
<pre class="line-numbers"><code class="language-javascript">1 ^ 1 = 0
1 ^ 0 = 1
0 ^ 1 = 1
0 ^ 0 = 0</code></pre>
<p>因此同位相加的運算, 完全可由按位異或(^)代替, : x^y.</p>
<p>那麼21 應該怎麼實現呢? 實際上, 非位移位運算中, 只有按位與(&amp;)才能滿足遇2的場景, 且只有有符號左移(&lt;&lt;)能滿足進1的場景.</p>
<p>現在範圍縮小了, 就看&amp;&lt;&lt;運算符能不能真正滿足需要了. 值得高興的是, 按位與(&amp;)只有在同位都是1的情況下才返回1, 其他情況均返回0. 如果對其運算結果再做左移一位的運算, : (x&amp;y)&lt;&lt;1. 剛好滿足了21的場景.</p>
<p>因爲我們是將同位相加和21的兩種運算分開進行. 那麼最終的加法運算結果應該還要做一次加法. 如下:</p>
<p>最終公式: <code class="language-javascript">x + y = x^y + (x&amp;y)&lt;&lt;1</code> </p>
<p>這個公式並不完美, 因爲它還是使用了加法, 推導公式怎麼能直接使用推導結果呢? 太可怕了, 就不怕掉入遞歸深淵嗎? 下面我們就來繞過這個坑. 而繞過這個坑有一個前提, 那就是隻要 x^y (x&amp;y)&lt;&lt;1中有一個值爲0就行了, 這樣便不用進行加法運算了. 講了這麼多, 不如看代碼.</p>
<pre class="line-numbers"><code class="language-javascript">function add(x, y){
   var _x = x^y,
   _y = (x&amp;y)&lt;&lt;1;
   return !_x &amp;&amp; _y || !_y &amp;&amp; _x || arguments.callee(_x,_y);
}
add(12345678,87654321);//999999999
add(9527,-12);//9515</code></pre>
<h3>總結</h3>
<p>最後補充一點: 位運算一般只適用 [-2^31, 2^31-1] (-2147483648~2147483647) 以內的正負數. 超過這個範圍, 計算將可能出現錯誤. 如下:</p>
<pre class="line-numbers"><code class="language-javascript">console.log(1&lt;&lt;31);//-2147483648 </code></pre>
<p>由於數值(2^31)超過了31(加上保留的一個符號位,32), 故計算出錯, 於是按照負數的方式解釋二進制的值了.說好的不改變符號呢!!!</p>
<p>本文囉嗦幾千字, 就爲了說清楚兩個事兒. ① Math對象中, 比較常用的就是數值運算方法, 不妨多看看, 其他的知道有這個api就行了. ② 位運算中, 則需要基本瞭解每種位運算符的運算方式, 如果能注意運算中 01等特殊數值 的一些妙用就更好了. 無論如何, 本文不可能面面俱到. 如果您對負數的位運算不甚理解, 建議去補下計算機的補碼. 希望能對您有所幫助.</p>
<h3>註解</h3>
<ol>
<li><p><code class="language-javascript">相反數</code> : 只有符號不同的兩個數, 我們就說其中一個是另一個的相反數.</p></li>
<li><p><code class="language-javascript">補碼</code>: 在計算機系統中, 數值一律用補碼來表示和存儲, 且正數的原碼和補碼相同, 負數的補碼等於其原碼按位取反再加1.</p></li>
</ol>
<hr>
<p>本問就討論這麼多內容, 如果您有什麼問題或好的想法歡迎在下方參與留言和評論.</p>
<p>本文作者: <a href="https://github.com/Louiszhai">louis</a></p>
<p>本文鏈接: <a href="http://louiszhai.github.io/2016/07/01/Math/">http://louiszhai.github.io/20...</a></p>
<p>參考文章</p>
<ul>
<li><p><a href="https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Math">Math - JavaScript | MDN</a></p></li>
<li><p><a href="http://www.cnblogs.com/xljzlw/p/4231354.html?utm_source=tuicool&amp;utm_medium=referral">js中位運算的運用 - 那時候的我 - 博客園</a></p></li>
<li><p><a href="http://blog.csdn.net/yunyu5120/article/details/6692072">編程技巧--位運算的巧妙運用</a></p></li>
<li><p><a href="http://www.jb51.net/article/32130.htm">c語言中用位運算實現加法技巧介紹_C 語言_腳本之家</a></p></li>
</ul>

發佈了79 篇原創文章 · 獲贊 21 · 訪問量 19萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章