Login With Github

Why Are The Numbers In JavaScript So Strange?

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:

  1. 0 - 51: fraction part (52 bits)
  2. 52 - 62: exponent part (11 bits)
  3. 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 with f, and the exponent part is represented with e;
  • 2 ^ (e - 1023) represents the e - 1023 power of 2
  • Since the fraction part occupies 52 bits, the value of f ranges from 00...00 (48 0's are omitted in the middle) to 11...11 ( 48 1's are omitted in the middle)
  • Since the exponent part occupies 11 bits, the value of e ranges from 0 (00000000000) to 2047 (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

temp