How JavaScript Bitwise Operations Work?

Backgrounds

It all starts with a question of the JavaScript bitwise operations shown below. What actually happens to the numbers when programs execute bitwise right shift operations? Why zero-fill right shift operations (triple greater than sign) applied to negative numbers would return a rather unintuitive result?

64 >> 2 // 16
-64 >> 2 // -16
64 >>> 2 // 16
-64 >>> 2 // 1073741807  Why?
In the subsequent sections, I will walk you through the whole process step by step to ensure you have a thorough understanding of all the details.

32-bit Signed Integer

Important points to note regarding operands of bitwise operations in JavaScript:
  • Operands are treated as 32-bit signed integer (two's complement representation) while taking part in the operation.

    // what you see
    2 | -1 = -1
    // what machine sees
    00000000000000000000000000000010
    | 11111111111111111111111111111111
    = 11111111111111111111111111111111
    
  • The minimum and the maximum integers that are representable using a 32-bit signed number are -2147483648 to 2147483647.
  • Operands that are out of the -2147483648 to 2147483647 range are actually converted (through truncating higher bits of the numbers) to ensure that they are in this range, before they take part in the operation.

Two's Complement

Two's complement is a mathematical operation to reversibly convert a positive binary number into a negative binary number with equivalent (but negative) value... (from Wikipedia)

Two's Complement can be seen as a good way to represent negative numbers which has a lot of benefits, compared with representing them using the original binary format.

The general steps to get the two's complement representation of a negative number(in decimal) is as below:

  1. Get the positive part(in decimal, denoted by B) of the targeting negative number(in decimal, denoted by A)
  2. Calculate the binary format of B, and we get C
  3. Invert every bit of C, and we get D
  4. Add 1 to D, and we get E

And E is indeed the two's complement representation of the targeting negative number.

Note: There's one exception: as 2147483647 is the maximum number 31 bits binary can represent(not 32 bit, the highest bit is sign bit), -2147483648's two's complement representation cannot be calculated by the above method.And it can be seen as a special case.

Below is a table of 32-bit signed integers in decimal and its corresponding two's complement representation.

DecimalBinary (two's complement)
0
0001110
10000101
11101111
01011110

----

----

2147483647
0
1111111
11111111
11111111
11111111
2147483646
0
1111111
11111111
11111111
11111110
2147483645
0
1111111
11111111
11111111
11111101
...

...

2
0
0000000
00000000
00000000
00000010
1
0
0000000
00000000
00000000
00000001
0
0
0000000
00000000
00000000
00000000
-1
1
1111111
11111111
11111111
11111111
-2
1
1111111
11111111
11111111
11111110
...

...

-2147483646
1
0000000
00000000
00000000
00000010
-2147483647
1
0000000
00000000
00000000
00000001
-2147483648
1
0000000
00000000
00000000
00000000

Bitwise Operators in JavaScript

There're 7 bitwise operators in JavaScript, as shown below.

Operator Name Example
~
Bitwise NOT
~a
&
Bitwise AND
a & b
|
Bitwise OR
a | b
^
Bitwise XOR
a ^ b
<<
Left shift
a << b
>>
Sign-propagating right shift
a >> b
>>>
Zero-fill right shift
a >>> b

And the keypoint of bitwise operation is that the decimal operand's two's complement representation is the "real" operand that will take part in the bitwise operation. You can input any qualified integers in the input area below to have a try and see what happens.

Bitwise NOT

Bitwise NOT operation is also known as binary One's complement.

Each bit of the binary value of the operand is inverted, that is to say, 1 to 0, and 0 to 1.

a
1
0111100
00101001
10101001
10100010
~a
1138120285
0
1000011
11010110
01010110
01011101

Bitwise AND

The bits of the same position of the binary values are compared using AND operation. If both bits are 1, then return 1, otherwise, return 0.

a
1
0100100
00010010
11111001
00101101
b
1
0110010
11111001
00011110
01010101
a & b
-1609558011
1
0100000
00010000
00011000
00000101

Bitwise OR

The bits of the same position of the binary values are compared using OR operation. If both bits are 0, then return 0, otherwise, return 1.

a
0
1100110
01010111
00101011
11100001
b
1
1011000
00011110
00001001
00011001
a | b
-27317255
1
1111110
01011111
00101011
11111001

Bitwise XOR

The bits of the same position of the binary values are compared using XOR operation. If both bits are different, then return 1, otherwise, return 0.

a
1
0001111
01110000
01011111
01100010
b
0
0001101
01001000
10110001
10110100
a ^ b
-2110198058
1
0000010
00111000
11101110
11010110

Bitwise Left Shift

The first operand's binary value is shifted left by X bits, where X is the second operand. The gaps are filled with 0, and the excess bits from the left are discarded.

a
1
0100011
00010110
00101101
01000010
b
0
0000000
00000000
00000000
00011001
a << b
-2080374784
1
0000100
00000000
00000000
00000000

Bitwise Sign-Propagating Right Shift

The first operand's binary value is shifted right by X bits, where X is the second operand. The excess bits from the right are discarded, and the leftmost bit keeps unchanged, and copies of the leftmost bit are shifted in from the left.

a
0
0100011
10100111
00111000
10011000
b
0
0000000
00000000
00000000
00000100
a >> b
37385097
0
0000010
00111010
01110011
10001001

Bitwise Zero-fill Right Shift

The first operand's binary value is shifted right by X bits, where X is the second operand. The excess bits from the right are discarded, and 0s are shifted in from the left.

a
1
0101010
11000111
10110001
01011000
b
0
0000000
00000000
00000000
00001110
a >>> b
174878
0
0000000
00000010
10101011
00011110
Note: When it comes to zero-fill right shift operation, the first operand is treated as a 32-bit signed integer (two's complement representation), while the result is interpreted as a 32-bit unsigned integer. Another special case that needs attention.

Back to the Questions At the Beginning

So now we know that why "-64 >>> 2" would result 1073741807. The reason is that -64 is a negative number, so in two's complement representation its sign bit is 1. ">>>" is zero-fill right shift operator, so after the shift, the sign bit is filled by 0, and then the new 32-bit number is interpreted as an unsigned 32-bit number, through which it was converted back to its decimal representation.

You can have a more realistic feel in the above "Bitwise Zero-fill Right Shift" section by filling the input field "a" with -64, and "b" with 0, and then add "b" one by one to see how the result would change.