这个话题的由来是2016年3月份的时候 NPM 社区发生了‘left-pad’事件,不久后社区就有人发布了用来补救的,也是如今你们能用到的 left-pad 库。javascript
最开始这个库的代码是这样的。html
module.exports = leftpad; function leftpad (str, len, ch) { str = String(str); var i = -1; if (!ch && ch !== 0) ch = ' '; len = len - str.length; while (++i < len) { str = ch + str; } return str; }
我第一次看到这段代码的时候,没看出什么毛病,以为清晰明了。后来刷微博的时候@左耳朵耗子老师指出了这段代码能够写得更有效率些,因而他就贴出了本身写的版本并给 left-pad 提了 PR,代码以下:java
module.exports = leftpad; function leftpad (str, len, ch) { //convert the `str` to String str = str +''; //needn't to pad len = len - str.length; if (len <= 0) return str; //convert the `ch` to String if (!ch && ch !== 0) ch = ' '; ch = ch + ''; var pad = ''; while (true) { if (len & 1) pad += ch; len >>= 1; if (len) ch += ch; else break; } return pad + str; }
我当时看到他的这段代码的里面的 &
和>>
运算符的时候一下有点懵了,只知道这是位运算里面的‘按位与’和‘右移’运算,可是彻底不知道为何这样写就能提升效率。因而就想着去了解位运算的实质和使用场景。npm
在了解位运算以前,咱们先必须了解一下什么是原码、反码和补码以及二进制与十进制的转换。app
一个数在计算机中是以二进制的形式存在的,其中第一位存放符号, 正数为0, 负数为1。原码就是用第一位存放符号的二进制数值。例如2的原码为00000010,-2的原码为10000010。lua
正数的反码是其自己。负数的反码是在其原码的基础上,符号位不变,其他各位取反,即0变1,1变0。spa
[+3]=[00000011]原=[00000011]反 [-3]=[10000011]原=[11111100]反
可见若是一个反码表示的是负数,并不能直观的看出它的数值,一般要将其转换成原码再计算。翻译
正数的补码是其自己。负数的补码是在其原码的基础上,符号位不变,其他各位取反,最后+1。(即负数的补码为在其反码的基础上+1)。code
[+3]=[00000011]原=[00000011]反=[00000011]补 [-3]=[10000011]原=[11111100]反=[11111101]补
可见对于负数,补码的表示方式也是让人没法直观看出其数值的,一般也须要转换成原码再计算。htm
二进制与十进制的区别在于数运算时是逢几进一位。二进制是逢2进一位,十进制也就是咱们经常使用的0-9是逢10进一位
正整数的十进制转二进制的方法为将一个十进制数除以2,获得的商再除以2,以此类推直到商等于1或0时为止,倒取除得的余数,即为转换所得的二进制数的结果。
例如把52换算成二进制数,计算过程以下图:
52除以2获得的余数依次为:0、0、一、0、一、1,倒序排列,因此52对应的二进制数就是110100。
负整数的十进制转二进制为将该负整数对应的正整数先转换成二进制,而后对其“取反”,再对取反后的结果+1。即负整数采用其二进制补码的形式存储。
至于负数为何要用二进制补码的形式存储,可参考一篇阮一峰的文章《关于2的补码》。
例如 -52 的原码为 10110100,其反码为 11001011,其补码为 11001100。因此 -52 转换为二进制后为 11001100。
十进制小数转二进制的方法为“乘2取整”,对十进制小数乘2获得的整数部分和小数部分,整数部分便是相应的二进制数码,再用2乘小数部分(以前乘后获得新的小数部分),又获得整数和小数部分。
如此不断重复,直到小数部分为0或达到精度要求为止。第一次所获得为最高位,最后一次获得为最低位。
如:0.25的二进制 0.25*2=0.5 取整是0 0.5*2=1.0 取整是1 即0.25的二进制为 0.01 ( 第一次所获得为最高位,最后一次获得为最低位) 0.8125的二进制 0.8125*2=1.625 取整是1 0.625*2=1.25 取整是1 0.25*2=0.5 取整是0 0.5*2=1.0 取整是1 即0.8125的二进制是0.1101
从最后一位开始算,依次列为第0、一、2...位,第n位的数(0或1)乘以2的n次方,将获得的结果相加就是获得的十进制数。
例如二进制为110的数,将其转为十进制的过程以下
个位数 0 与 2º 相乘:0 × 2º = 0
十位数 1 与 2¹ 相乘:1 × 2¹ = 2
百位数 1 与 2² 相乘:1 × 2² = 4
将获得的结果相加:0+2+4=6
因此二进制 110 转换为十进制后的数值为 6。
小数二进制用数值乘以2的负幂次而后相加。
在 ECMAScript 中按位操做符会将其操做数转成补码形式的有符号32位整数。下面是ECMAScript 规格中对于位运算的执行过程的表述:
The production A : A @ B, where @ is one of the bitwise operators in the productions above, is evaluated as follows: 1. Let lref be the result of evaluating A. 2. Let lval be GetValue(lref). 3. ReturnIfAbrupt(lval). 4. Let rref be the result of evaluating B. 5. Let rval be GetValue(rref). 6. ReturnIfAbrupt(rval). 7. Let lnum be ToInt32(lval). 8. ReturnIfAbrupt(lnum). 9. Let rnum be ToInt32(rval). 10. ReturnIfAbrupt(rnum). 11. Return the result of applying the bitwise operator @ to lnum and rnum. The result is a signed 32 bit integer.
须要注意的是第七步和第九步,根据 ES 的标准,超过32位的整数会被截断,而小数部分则会被直接舍弃。因此由此能够知道,在 JS 中,当位运算中有操做数大于或等于2³²时,就会出现意想不到的结果。
JavaScript 中的位运算有:&(按位与)
、|(按位或)
、~(取反)
、^(按位异或)
、<<(左移)
、>>(有符号右移)
和>>>(无符号右移)
。
对每个比特位执行与(AND)操做。只有 a 和 b 都是 1 时,a & b 才是 1。
例如:9(base 10) & 14(base 10) = 1001(base2) & 1110(base 2) = 1000(base 2) = 8(base 10)
由于当只有 a 和 b 都是 1 时,a&b才等于1,因此任一数值 x 与0(二进制的每一位都是0)按位与操做,其结果都为0。将任一数值 x 与 -1(二进制的每一位都是1)按位与操做,其结果都为 x。
利用 & 运算的特色,咱们能够用以简单的判断奇偶数,公式:
(n & 1) === 0 //true 为偶数,false 为奇数。
由于 1 的二进制只有最后一位为1,其他位都是0,因此其判断奇偶的实质是判断二进制数最后一位是 0 仍是 1。奇数的二进制最后一位是 1,偶数是0。
固然还能够利用 JS 在作位运算时会舍弃掉小数部分的特性来作向下取整的运算,由于当 x 为整数时有 x&-1=x
,因此当 x 为小数时有 x&-1===Math.floor(x)
。
对每个比特位执行或(OR)操做。若是 a 或 b 为 1,则 a | b 结果为 1。
例如:9(base 10) | 14(base 10) = 1001(base2) | 1110(base 2) = 1111(base 2) = 15(base 10)
由于只要 a 或 b 其中一个是 1 时,a|b就等于1,因此任一数值 x 与-1(二进制的每一位都是1)按位与操做,其结果都为-1。将任一数值 x 与 0(二进制的每一位都是0)按位与操做,其结果都为 x。
一样,按位或也能够作向下取整运算,由于当 x 为整数时有 x|0=x
,因此当 x 为小数时有 x|0===Math.floor(x)
。
对每个比特位执行非(NOT)操做。~a 结果为 a 的反转(即反码)。
9 (base 10) = 00000000000000000000000000001001 (base 2) -------------------------------- ~9 (base 10) = 11111111111111111111111111110110 (base 2) = -10 (base 10)
负数的二进制转化为十进制的规则是,符号位不变,其余位取反后加 1。
对任一数值 x 进行按位非操做的结果为 -(x + 1)。~~x === x。
一样,取反也能够作向下取整运算,由于当 x 为整数时有 ~~x===x
,因此当 x 为小数时有 ~~x===Math.floor(x)
。
对每一对比特位执行异或(XOR)操做。当 a 和 b 不相同时,a ^ b 的结果为 1。
例如:9(base 10) ^ 14(base 10) = 1001(base2) ^ 1110(base 2) = 0111(base 2) = 7(base 10)
将任一数值 x 与 0 进行异或操做,其结果为 x。将任一数值 x 与 -1 进行异或操做,其结果为 ~x,即 x^-1=~x。
一样,按位异或也能够作向下取整运算,由于当 x 为整数时有 (x^0)===x
,因此当 x 为小数时有 (x^0)===Math.floor(x)
。
它把数字中的全部数位向左移动指定的数量,向左被移出的位被丢弃,右侧用 0 补充。
例如,把数字 2(等于二进制中的 10)左移 5 位,结果为 64(等于二进制中的 1000000):
var iOld = 2; //等于二进制 10 var iNew = iOld << 5; //等于二进制 1000000 十进制 64
由于二进制10转换成十进制的过程为 1×2¹+0×2º,在运算中2的指数与位置数相对应,当左移五位后就变成了 1×2¹⁺⁵+0×2º⁺⁵= 1×2¹×2⁵+0×2º×2⁵ = (1×2¹+0×2º)×2⁵。因此由此能够看出当2左移五位就变成了 2×2⁵=64。
因此有一个数左移 n 为,即为这个数乘以2的n次方。x<<n === x*2ⁿ
。
一样,左移运算也能够作向下取整运算,由于当 x 为整数时有 (x<<0)===x
,因此当 x 为小数时有 (x<<0)===Math.floor(x)
。
它把 32 位数字中的全部数位总体右移,同时保留该数的符号(正号或负号)。有符号右移运算符刚好与左移运算相反。例如,把 64 右移 5 位,将变为 2。
由于有符号右移运算符与左移运算相反,因此有一个数左移 n 为,即为这个数除以2的n次方。x<<n === x/2ⁿ
。
一样,有符号右移运算也能够作向下取整运算,由于当 x 为整数时有 (x>>0)===x
,因此当 x 为小数时有 (x>>0)===Math.floor(x)
。
它将无符号 32 位数的全部数位总体右移。对于正数,无符号右移运算的结果与有符号右移运算同样,而负数则被做为正数来处理。
-9 (base 10): 11111111111111111111111111110111 (base 2) -------------------------------- -9 >>> 2 (base 10): 00111111111111111111111111111101 (base 2) = 1073741821 (base 10)
根据无符号右移的正数右移与有符号右移运算同样,而负数的无符号右移必定为非负的特征,能够用来判断数字的正负,以下:
function isPos(n) { return (n === (n >>> 0)) ? true : false; } isPos(-1); // false isPos(1); // true
根据 JS 的位运算,能够得出以下信息:
一、全部的位运算均可以对小数取底。
二、对于按位与&
,能够用 (n & 1) === 0 //true 为偶数,false 为奇数。
来判断奇偶。用x&-1===Math.floor(x)
来向下取底。
三、对于按位或|
,能够用x|0===Math.floor(x)
来向下取底。
四、对于取反运算~
,能够用~~x===Math.floor(x)
来向下取底。
五、对于异或运算^
,能够用(x^0)===Math.floor(x)
来向下取底。
六、对于左移运算<<
,能够x<<n === x*2ⁿ
来求2的n次方,用x<<0===Math.floor(x)
来向下取底。
七、对于有符号右移运算>>
,能够x<<n === x/2ⁿ
求一个数字的 N 等分,用x>>0===Math.floor(x)
来向下取底。
八、对于无符号右移运算>>>
,能够(n === (n >>> 0)) ? true : false;
来判断数字正负,用x>>>0===Math.floor(x)
来向下取底。
用移位运算来替代普通算术能得到更高的效率。移位运算翻译成机器码的长度更短,执行更快,须要的硬件开销更小。
原文连接:由left-pad扯到JS中的位运算