Why Are The Numbers In JavaScript So Strange?
- ·
- 30 Jan 2019
- ·
- ·
- 3304 Views
Note: The knowledge of binary is needed to read this article.
Many JavaScript developers may have come across the strange phenomena of dealing with the numbers in JavaScript, such as:
> 0.1 + 0.2
0.30000000000000004
> 0.1 + 1 - 1
0.10000000000000009
> 0.1 * 0.2
0.020000000000000004
> Math.pow(2, 53)
9007199254740992
> Math.pow(2, 53) + 1
9007199254740992
> Math.pow(2, 53) + 3
9007199254740996
If you want to explore the reason, first of all, you need to figure out how numbers are encoded in JavaScript.
1. How Numbers Are Encoded In JavaScript?
The numbers in JavaScript, no matter they are integers, fractions, or positive or negative numbers, are all float numbers, which are stored in a binary format, in 8 bytes (64 bits).
A number (such as 12
, 0.12
, -999
) occupies 8 bytes (64 bits) in memory and is stored in the way as follows:
0 - 51
: fraction part (52 bits)52 - 62
: exponent part (11 bits)63
: sign bit (1 bit: 0 means the number is positive, 1 means the number is negative)
The sign bit is easy to understand and is used to indicate whether the number is positive or negative. And it has only 1 bit for two cases (0 for the positive number and 1 for the negative number).
The other two parts are the fraction part and the exponent part, which are used to calculate the absolute value of a number.
1.1 Formula for calculating absolute values
1: abs = 1.f * 2 ^ (e - 1023) 0 < e < 2047
2: abs = 0.f * 2 ^ (e - 1022) e = 0, f > 0
3: abs = 0 e = 0, f = 0
4: abs = NaN e = 2047, f > 0
5: abs = ∞ (infinity) e = 2047, f = 0
- It is a binary calculation formula, and the result is represented with
abs
, the fraction part is represented withf
, and the exponent part is represented withe
;2 ^ (e - 1023)
represents thee - 1023
power of2
;- Since the fraction part occupies 52 bits, the value of
f
ranges from00...00
(480
's are omitted in the middle) to11...11
( 481
's are omitted in the middle)- Since the exponent part occupies 11 bits, the value of
e
ranges from0
(00000000000
) to2047
(11111111111
).So:
1
is stored as:1.00 * 2 ^ (1023 - 1023)
(f = 0000..., e = 1023
,...
represents 48 0's)- 2 is stored as:
1.00 * 2 ^ (1024 - 1023)
(f = 0000..., e = 1024
,...
represents 48 0's)- 9 is stored as:
1.01 * 2 ^ (1025 - 1023)
(f = 0100..., e = 1025
,...
represents 48 0's)- 0.5 is stored as:
1.00 * 2 ^ (1022 - 1023)
(f = 0000..., e = 1022
,...
represents 48 0's)- 0.625 is stored as:
1.01 * 2 ^ (1021 - 1023)
(f = 0100..., e = 1021
,...
represents 48 0's)
1.2 Range and boundary of absolute values
What can be seen from the above formula is:
1.2.1 0 < e < 2047
When 0 < e < 2047
, the range of values is from f = 0, e = 1
to f = 11...11, e = 2046
(48 1's are omitted in the middle)
The above means that the range is from Math.pow(2, -1022)
to ~= Math.pow(2, 1024) - 1
(~=
represents approximately equal)
Here, ~= Math.pow(2, 1024) - 1
is the value of Number.MAX_VALUE
, which is the maximum value you can represent in JavaScript.
1.2.2 e = 0, f > 0
When e = 0, f > 0
, the range of values is from f = 00...01, e = 0
(48 0's are omitted in the middle) to f = 11...11, e = 0
(48 1's are omitted in the middle)
The above means that the range is from: Math.pow(2, -1074)
to ~= Math.pow(2, -1022)
(~=
represents approximately equal)
Here, Math.pow(2, -1074)
is the value of Number.MIN_VALUE
, which is the minimum value (the absolute value) you can represent in JavaScript.
1.2.3 e = 0, f = 0
It only represents the value of 0
, but there are +0
and -0
if you take with the sign bit.
However in the operation:
> +0 === -0
true
1.2.4 e = 2047, f > 0
It only represents the value of NaN
.
But in the operation:
> NaN == NaN
false
> NaN === NaN
false
1.2.5 e = 2047, f = 0
This only represents the value of ∞
(infinity).
In the operation:
> Infinity === Infinity
true
> -Infinity === -Infinity
true
1.3 Maximum Safe Value Of Absolute Values
As you can see from the above, the maximum value that can be stored in 8 bytes is the value of Number.MAX_VALUE
, which is ~= Math.pow(2, 1024) - 1
.
But the values are not safe: the numbers in the range from 1
to Number.MAX_VALUE
are not continuous, but discrete.
For example: The values of Number.MAX_VALUE - 1
, Number.MAX_VALUE - 2
and so on can't be formulated, thus they can't be stored.
So here comes the maximum safe value, Number.MAX_SAFE_INTEGER
, which means that the numbers from 1
to Number.MAX_SAFE_INTEGER
are continuous, so the numerical calculations in this range are safe.
You can get the value 111...11
(48 1's are omitted in the middle), ie Math.pow(2, 53) - 1
when f = 11...11, e = 1075
(48 1's are omitted in the middle).
Values greater than Number.MAX_SAFE_INTEGER:Math.pow(2, 53) - 1
are discrete.
For example: Math.pow(2, 53) + 1
, Math.pow(2, 53) + 3
can't be formulated and can't be stored in memory.
So it is the reason why you feel the code at the beginning of this article is so strange:
> Math.pow(2, 53)
9007199254740992
> Math.pow(2, 53) + 1
9007199254740992
> Math.pow(2, 53) + 3
9007199254740996
Math.pow(2, 53) + 1
can't be formulated, so it can't be stored in memory. Thus you have to get another number which can be formulated as well as is closest. Here the obtained number is Math.pow(2, 53)
, and it can be stored in memory then. This leads to distortion, so it is not safe.
1.4 Storage And Calculation Of Decimal Fractions
Only the decimal fractions that satisfies the condition of m / (2 ^ n)
(m, n
are integers) can be represented by the binary completely. The others can't be represented by the binary completely, and only can be represented by approaching the binary fraction infinitely..
(Note: [2]
means binary)
0.5 = 1 / 2 = [2]0.1
0.875 = 7 / 8 = 1 / 2 + 1 / 4 + 1 / 8 = [2]0.111
Approximation of 0.3:
0.25 ([2]0.01) < 0.3 < 0.5 ([2]0.10)
0.296875 ([2]0.0100110) < 0.3 < 0.3046875 ([2]0.0100111)
0.2998046875 ([2]0.01001100110) < 0.3 < 0.30029296875 ([2]0.01001100111)
//... It'll calculate according to the formula until the 52 bits of the fractional part are filled, and then take the nearest number.
Storage for 0.3:[2]0.010011001100110011001100110011001100110011001100110011
(f = 0011001100110011001100110011001100110011001100110011, e = 1021)
What can be seen from the above is that most of the decimals are only approximations, and only a small part is the true value, so only the values (the decimals that satisfy the condition of m / (2 ^ n)
) from this small part can be compared directly, while the others can't be compared directly.
> 0.5 + 0.125 === 0.625
true
> 0.1 + 0.2 === 0.3
false
In order to compare two decimals safely, we can introduce Number.EPSILON [Math.pow(2, -52)]
to compare floating point numbers.
> Math.abs(0.1 + 0.2 - 0.3) < Number.EPSILON
true
1.5 Maximum Reserved Digits For Decimals
It retains a maximum of 17 significant digits when JavaScript reads a number from memory.
> 0.010011001100110011001100110011001100110011001100110011
0.30000000000000000
0.3
> 0.010011001100110011001100110011001100110011001100110010
0.29999999999999993
> 0.010011001100110011001100110011001100110011001100110100
0.30000000000000004
> 0.0000010100011110101110000101000111101011100001010001111100
0.020000000000000004
2. Constants In The Number Object
2.1 Number.EPSILON
The difference between 1 and the smallest floating point number represented by Number and greater than 1.
Math.pow(2, -52)
It's used for safe comparison between floating point numbers.
2.2 Number.MAX_SAFE_INTEGER
The maximum safe value of the absolute value.
Math.pow(2, 53) - 1
2.3 Number.MAX_VALUE
The maximum value that js can represent (the maximum value that can be stored in 8 bytes).
~= Math.pow(2, 1024) - 1
2.4 Number.MIN_SAFE_INTEGER
The minimum safe value (including the sign symbol).
-(Math.pow(2, 53) - 1)
2.5 Number.MIN_VALUE
The minimum value (absolute value) that js can represent.
Math.pow(2, -1074)
2.6 Number.NEGATIVE_INFINITY
Negative infinity.
-Infinity
2.7 Number.POSITIVE_INFINITY
Positive infinity.
+Infinity
2.8 Number.NaN
It's non-numeric.
3. The Cause Of The strange Phenomenon
3.1 Why the result of 0.1 + 0.2 is 0.30000000000000004
It's similar to the approximation algorithm for 0.3
.
Storage for 0.1:[2]0.00011001100110011001100110011001100110011001100110011010
(f = 1001100110011001100110011001100110011001100110011010, e = 1019)
Storage for 0.2:[2]0.0011001100110011001100110011001100110011001100110011010
(f = 1001100110011001100110011001100110011001100110011010, e = 1020)
0.1 + 0.2: 0.0100110011001100110011001100110011001100110011001100111
(f = 00110011001100110011001100110011001100110011001100111, e = 1021)
However, there are 53 bits for f = 00110011001100110011001100110011001100110011001100111
, which exceeds the normal 52 bits and cannot be stored, so it takes the nearest number:
0.1 + 0.2: 0.010011001100110011001100110011001100110011001100110100
(f = 0011001100110011001100110011001100110011001100110100, e = 1021)
Thus Js reads this number as 0.30000000000000004
3.2 Why the result of Math.pow(2, 53) + 1 is Math.pow(2, 53)
Math.pow(2, 53) + 1
cannot be formulated and cannot be stored in memory, so it has to take the number that can be formulated and is closest.
The closest and smaller number:
Math.pow(2, 53)
(f = 0000000000000000000000000000000000000000000000000000, e = 1076)
The closest and bigger number:
Math.pow(2, 53) + 2
(f = 0000000000000000000000000000000000000000000000000001, e = 1076)
Take the first number: Math.pow(2, 53)
.
So:
> Math.pow(2, 53) + 1 === Math.pow(2, 53)
true
0 Comment
Login to post a comment
Login With Github