全面解读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/…

参考文章

相关文章
相关标签/搜索