Reference
Articles
- How numbers are encoded in JavaScript — Dr. Axel Rauschmayer
- What You Need to Know About JavaScript Number Type — Max Wizard K
- https://indepth.dev/posts/1018/the-mechanics-behind-exponent-bias-in-floating-point
- https://indepth.dev/posts/1019/the-simple-math-behind-decimal-binary-conversion-algorithms
- What Every JavaScript Developer Should Know About Floating Point Numbers — Chewxy
- The Secret Life of JavaScript Primitives — Angus Croll
- https://includestdio.tistory.com/26
- (Not) Everything in JavaScript is an Object — Daniel Li
- Diving Deeper in JavaScripts Objects — Arfat Salman Later
- The differences between Object.freeze() vs Const in JavaScript — Bolaji Ayodeji Later
Definition
Data type except object which is immutable.
There are 6 types in primitive types. Boolean type represents the logical entitiy which is true or false. Null type is just null. Undefined type represents when variable has not been assigned a value. Symbol type is an unique value which can be used as key of an object property. String type represents the textual data. BigInt type represent numeric value that exceeds safe integer limit for Number type.
Number type
Number type represents all numeric value; integers, floating-point number, +Infinity , -Infinity, NaN. Values are range from -(2^53-1) ~ (2^53-1) represented in 64 bit IEEE(IEEE 754 Double Precision) format.
0.1 + 0.2 == 0.3 // false
Through this example, I will illustrate how number is stored and evaluated in Javascript.
Why 0.1
+ 0.2
differs from 0.3
?
1. Scientific notation
$$significant \times base^{exponent}$$
- significant: number of significant digits. It's also called Mantissa or Precision.
- base: numeric system base.
- exponent: how many places a radix point moved from the original number.
A normalized form is a number which is represented as a scientific notation with
- one nonzero decimal digit before radix point
ex) 1.1 x 10^(-2), 1.01 x 2^(-2)
In binary base, normalized format always has 1 before radix point.
2. how numbers are stored
There are two types; single precision and double precision. As javascript uses double precision, the total bits are 64, sign takes 1bit, exponent takes 11 bits, and significant takes 52bits.
sign
if number is positive, 0 and negative, 1.exponent
Exponent is stored in offset binary format. Offset binary format works in following steps.
1) Find offset K which, $$K = 2^{n-1}-1$$ for n bits.
2) add K to given number
3) convert it to binary
Advantage compare to two's component is that we can compare number using lexicographical order.
Ex) convert 3 in 4 bits using offset binary format.
1. K = 7
2. 3+7 = 10
3. 10 = 1010_(2)
- significant
As we only consider binary normalized form, 1 always exists before radix point. So we ignore the most front
1
.
3. convert decimal fraction to binary
It's better to show in example.
Ex) convert 0.375 in binary form.
- 0.375 X 2 = 0 + 0.75
- 0.75 X 2 = 1 + 0.5
- 0.5 X 2 = 1 + 0
-> 0.375_(10) = 0.011_(2) = 0 X 2^-1 + 1 X 2^-2 + 1 X 2^-3
4. Calculating 0.1 + 0.2
first transform to binary
0.1 X 2 = 0 + 0.2
0.2 X 2 = 0 + 0.4
0.4 x 2 = 0 + 0.8
0.8 x 2 = 1 + 0.6
0.6 x 2 = 1 + 0.2
0.2 x 2 = 0 + 0.4
...
-> 0.1_(10) = 0.0001100110011..._(2) = $$0.0\overline{0011}$$_(2)-
represent into normalized scientific notation
0.0001100110011...
= 1.1001100110011..._(2) x 2^(-4)
= $$1.\overline{1001}$$ x 2^(-4)- transform to IEEE 754 form
-
exponent
K = 2^(11-1) - 1 = 1023, as exponent is -4, -4 + 1023 = 1019
1019_(10) = 01111111011_(2) significant
ignore
1
before readix
-> 100110011001...
As number exceed 52bits, we should round number.
-> 10011001...10011010 (52bits)
... in total 0.1 =
0 01111111011 10011001...10011010_(2)
Using same method, we can calculate 0.2, which is
0 01111111100 10011001...10011010_(2)
We add these number in a scientific notation form. With all these procedures, we have rounded number 3 times in total. However, if we just transform 0.3 into binary IEEE 754 form, we just round 1 time. So, this is why there's difference between two values.
Number.MAX_SAFE_INTEGER
Definition
Largest integer n such that n and n+1 are both exactly representable as Number value.
Number.MAX_SAFE_INTEGER = 9007199254740991 = $$2^{53}-1$$
But, it's not the largest integer that can be represented.
Number.MAX_SAFE_INTEGER = 9007199254740992 = 1111...111(53bits)
= 1.111...11(52bits) x $$2^{52}$$
= 0 10000110011 1111...1111(52bits)
We used all places in significant part. Thats why it's the largest value that represented properly.
What happens to number beyond Number.MAX_SAFE_INTEGER
Straight to the point, only even integer beyond Number.MAX_SAFE_INTEGER can be represented. Odd numbers are rounded to the nearest even number. Why?
Number.MAX_SAFE_INTEGER+1 = 9007199254740992 = 1.111...11(52bits) x $$2^{52}$$ + 1
= 1.000..00(52bits) x $$2^{53}$$
This is okay. However, as the significant part is full with 52bits, the only way to represent bigger number is using exponent part. Using exponent part means that we add number multiplied by 2.
For example,
1.0000...1(52bits) x $$2^{53}$$ == 1000...0010_(2) (53bits) = 9007199254740994_(10)
NaN & Infinity
NaN and Infinity are both expressed in IEEE form. NaN has exponent part full of 1's and significant part doesn't matters except it is all 0's. This is why we get
console.log(NaN == NaN) // false
Because, NaN is not fixed number.
Infinity has exponent part full of 1's and all 0's in significant part.
Wrapper objects
As primitive data types are not object, We cannot access its properties or methods. However, it is possible.
name = 'John';
name.length; // 4
name.indexOf('J'); // 0
Why does this happens?
If we treat primitve value like an object(access to method), Javascript automatically creats a wrapper object that wrap this value as an object. This object is used for while and discarded right away. This is called Auto-boxing.
Wrapper objects for number values are instances of Number, string values are wrapped by instances of String and the type for a boolean’s wrapper is Boolean. As you can see, null
and undefined
cannot be wrapped.
typeof "abc"; //"string"
typeof String("abc"); //"string"
typeof new String("abc"); //"object"
typeof (new String("abc")).valueOf(); //"string"
Also, we can store wrapper object by assigning it to a variable.
const pet = new String("dog")
typeof pet; // "object"
pet === "dog"; // false
Top comments (1)
It's really a good article with detailed explanation. Thanks for writing this :)