As stated in the MDN Docs, and here I quote,
The operands of all bitwise operators are converted to signed 32-bit integers in two's complement format.
This means tha when you apply a bitwise operator, for instance ~
, to 2.1
it is first converted to an integer, and only then is the operator applied. This effectively achieves the rounding down (floor) effect for positive numbers.
As to why these operators are used, instead of the much nicer to grasp Math.floor
, there are two main reasons. For one, these operators may be considerably faster to achieve the same result. Besides performance, some people just want the shortest code possible. All three operators you mentioned achieve the same effect, but ~~
just happens to be the shortest, and arguably the easiest to remember.
Given that the float to integer conversion happens before the bitwise operators are applied, let's see what happens with ~~
. I'll represent our target number (2, after the convertion from 2.1) using 8 bits, instead of 32, for shortness.
2: 0000 0010
~2: 1111 1101 (-3)
~~2: 0000 0010
So, you see, we apply an operator to retrieve only the integer part, but we can't apply only one bitwise not, because it would mess up the result. We revert it to the desired value applying the second operator.
Regarding your last example, take into account that the number you're testing with, 2e+21, is a relatively big number. It's a 2 followed by twenty-one zeroes. It simply doesn't fit as a 32-bit integer (the data-type it is being converted to, when you apply the bitwise operators). Just look at the difference between your number and what a 32-bit signed integer can represent.
Max. Integer: 2147483647
2e+21: 2000000000000000000000
How about binary?
Max. Integer: 01111111111111111111111111111111
2e+21: 11011000110101110010011010110111000101110111101010000000000000000000000
Quite big, huh?
What really happens under the hood is that Javascript is truncating your big number to what it can represent in 32 bits.
110110001101011100100110101101110001011 10111101010000000000000000000000
^---- Junk ----^
When we convert our truncated number to decimal, we get back what you're seeing.
Bin: 10111101010000000000000000000000
Dec: -1119879168
Conversely, Math.floor
accounts for the big numbers and avoids truncating them, which is one of the possible reasons for it being slower, although accurate.