全面解读Math对象及位运算

2,859 阅读25分钟

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/…

参考文章