深入理解JavaScript位运算符
在开始聊位运算符之前,我们需要先来聊一聊二进制,因为位运算与二进制是密不可分的。
二进制
所谓的二进制,其实简单点理解就是以32位数值来表示一串十进制数值的方式吧。因为我们现在程序里面用到的都是十进制数值,但是计算机内部计算会把十进制转换成二进制再进行计算。
我们都知道,整数有两种类型的,既:正数、负数。其实在二进制里面,它认为整数有两种类型,既有符号整数(也就是刚刚说的正数和负数)和无符号整数(其实就是正数,没有写+号罢了)。那么,二进制是如何表示一个十进制的数值呢?
我们刚刚说过,二进制是有32位数值来表示一个十进制数值的。其实有符号整数是使用31位数值来表示整数的数值,用第32位来表示整数的符号,0表示为正数,1表示为负数。而数值范围是从-2147483648到2147483647。
在存储数值的时候,是以两种不同方式来存储二进制形式的有符号整数:一种是存储正数、一种是存储负数。正数是以真二进制形式存储的,前31位中的每一位都表示2的幂,从第1位(位0)开始,表示 2的0次幂,第2位(位1)表示 2的1次幂,依次类推...。没用到的位用0填充,即忽略不计。
在网上找了张图,可以帮助大家理解一下
从图中可以看到,开始位是在右边开始的,末位是在左边,所以这点是要注意的地方。
上图中是以数值18的二进制来做的示例,其有效位是前五位,即10010。我们用代码做个转换其实就可以看到有效位
let num = 18
console.log(num.toString(2)) // 10010
那么我们如何把二进制转换成十进制的呢?上面已经说了计算方法,下面我们来用代码做个换算,更深入的理解一下
// 18的二进制表示 10010,即我们从二进制右边到左边的幂次计算是这样的
(Math.pow(2, 4) * 1) + (Math.pow(2, 3) * 0) + (Math.pow(2, 2) * 0) + (Math.pow(2, 1) * 1) + (Math.pow(2, 0) * 0) = 18
根据上方的示例,我们就很清楚的看到了,从右至左分别从数值10010右边第一位进行按2的幂次相乘分别相加,最后得出十进制数值。http://blog.cuteur.cn/
顺便我们再看一张我从网络上找的图,来加强理解吧
其实负数也存储为二进制代码,不过采用的形式是二进制补码。计算数字二进制补码有三个步骤:
- 确定该数字的非负版本的二进制表示(例如,要计算-18的二进制补码,首先要确定18的二进制表示)
- 求得二进制反码,即需把 0 替换为 1,把 1 替换为 0
- 在二进制反码上加 1
到这,我们就讲完了二进制相关的东西了,下面我们就开始讲讲位运算符。
位运算符
按位非(NOT):~
它的运算是取数值二进制的反码,然后将反码二进制数转成浮点数。反码的意思就是将二进制0和1的数值反转,上面已经说过了。
例如:
let num = 5 // 5的二进制 00000000000000000000000000000101
let num1 = ~5 // 取5的二进制反码 11111111111111111111111111111010
console.log(num1) // 最后得出 -6
换种方式理解,其实按位非NOT:~ 实际上是在给数值求负,然后减一。其实用下面这种方式同样可以得出以上结果
let num = 5
let num1 = -num - 1
console.log(num1) // 得出 -6
按位与(AND): &
它的运算规则是将两个操作数(二进制形式)的每一位对齐,跟据以下规则进行计算。
- 只有同是1的时候,结果才是1
- 其它任何情况都是0
例如:
let num = 5 & 10
/*
5的二进制:00000000000000000000000000000101
10的二进制:00000000000000000000000000001010
-------------------------------------------
运算得出:00000000000000000000000000000000
*/
// 最后将运算得出的二进制转为十进制,即按照上方说的计算方式计算
console.log(num) // 0
按位或(OR): |
同样它的运算规则也是将两个操作数(二进制形式)的每一位对齐,然后跟据以下规则进行计算。
- 只有同是0的时候,结果才是0;
- 其它任何情况都是1;
例如:
let num = 5 | 10
/*
5的二进制:00000000000000000000000000000101
10的二进制:00000000000000000000000000001010
-------------------------------------------
运算得出:00000000000000000000000000001111
*/
// 最后将运算得出的二进制转为十进制,即按照上方说的计算方式计算
Math.pow(2, 3) * 1 + Math.pow(2, 2) * 1 + Math.pow(2, 1) * 1 + Math.pow(2, 0) * 1
console.log(num) // 最后得出 15
按位异或(XOR): ^
它的运算规则同样是将两个操作数(二进制形式)的每一位对齐,然后跟据以下规则进行计算。
- 两位同是0或1的时候,结果才是0;
- 其它任何情况都是1;
例如:
let num = 5 ^ 4
/*
5的二进制:00000000000000000000000000000101
4的二进制:00000000000000000000000000000100
-------------------------------------------
运算得出: 00000000000000000000000000000001
*/
// 最后将运算得出的二进制转为十进制,即按照上方说的计算方式计算
Math.pow(2, 0) * 1
console.log(num) // 得出 1
左移:<<
这个操作符会将数值的所有位向左移动指定的位数。右边空出来的位置,补0;
例如:
let num = 5 << 4
/*
5 的二进制: 00000000000000000000000000000101
---------------------------------------------
向左移动四位: 00000000000000000000000001010000
*/
// 最后将移动得出的二进制转为十进制,即按照上方说的计算方式计算
Math.pow(2, 6) * 1 + Math.pow(2, 5) * 0 + Math.pow(2, 4) * 1
console.log(num) // 得出 80
有符号右移:>>
这个操作符会将数值向右移动,但保留符号位(即正负号标记)。有符号的右移操作与左移操作恰好相反。
例如:
let num = 8 >> 3
/*
8 的二进制: 00000000000000000000000000001000
---------------------------------------------
向右移动三位: 0 0000000000000000000000000000001
第32位保持不动,从第31位开始往后推3位
*/
// 最后将移动得出的二进制转为十进制,即按照上方说的计算方式计算
Math.pow(2, 0) * 1
console.log(num) // 得出 1
无符号右移:>>>
这个操作符会将数值的所有32位都向右移动,对正数来说,无符号右移的结果与有符号右移相同。
但是对负数来说,就不一样了。首先,无符号右移是以0来填充空位,而不是像有符号右移那样以符号位之前的值来填充空位。所以,对正数的无符号右移与有符号右移结果相同,但对负数的结果就不一样了。
其次,无符号右移操作符会把负数的二进制码当成正数的二进制码。
而且,由于负数以其绝对值的二进制补码形式表示,因此就会导致无符号右移后的结果非常大。
例如:
let num = -14 >>> 2
/*
-14 的二进制:11111111111111111111111111110010
---------------------------------------------
向右移动2位: 00111111111111111111111111111100
*/
// 最后将移动得出的二进制转为十进制,即按照上方说的计算方式计算
// 由于数值过多,计算过长。所以这里我就直接用函数递归方式计算
function sum(n = 29, a = 0) {
if (n > 1) {
let d = Math.pow(2, n) * 1
a = a + d
return sum(--n, a)
}
return a
}
sum() // 1073741820
console.log(num) // 得出 1073741820