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方法的一些規律
以上, 數值運算中, 存在如下規律:
- 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;
- 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
. - 稍微利用 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)按位與運算返回數值自身.
- 2的整數次方的值與它的相對數按位與運算返回它自身.
- 任意整數與0進行按位與運算, 都將會返回0.
- 任意整數與1進行按位與運算, 都只有0 或1 兩個返回值.
- 按位與運算的結果不大於兩數中的最大值.
由公式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的要求較為嚴苛, 因此, 它是一種趨向增大最小值的運算. 對於按位或(|)運算, 滿足如下規律:
- 數值與自身按位或運算返回數值自身.
- 2的整數次方的值與它的相對數按位或運算返回它的相對數.
- 任意整數與0進行按位或運算, 都將會返回它本身.
- 任意整數與-1進行按位或運算, 都將返回-1.
- 按位或運算的結果不小於兩數中的最小值.
稍微利用公式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複製程式碼
對於按位異或(^)操作, 滿足如下規律:
- 由於按位異或位運算的特殊性, 數值與自身按位異或運算返回0. 如:
8^8=0
, 公式為a^a=0
. - 任意整數與0進行按位異或運算, 都將會返回它本身. 如:
0^-98=-98
, 公式為0^a=a
. - 任意整數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次方
. - 任意整數x與-1(負2的1次方+1)進行按位異或運算, 則將返回
-x-1
, 相當於~x運算 . 如:-1^100=-101
,-1^-9=8
. 公式為-1^x=-x-1=~x
. - 任意整數連續按位異或兩次相同的數值, 返回它本身. 如:
3^8^8=3
, 公式為a^b^b=a
或a^b^a=b
. - 按位異或滿足運算元與運算結果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.
本問就討論這麼多內容, 如果您有什麼問題或好的想法歡迎在下方參與留言和評論.
本文作者: louis
本文連結: louiszhai.github.io/2016/07/01/…
參考文章