The Mozilla developer community has been an important source of learning for me, and one time I came across the API and saw a few lines of code from Polyfill:

var list = Object(this);
var length = list.length >>> 0;
Copy the code

List. Length >>> 0 unsigned right shift to list. Length returns an integer >=0, but the operation process behind it is not clear. Review the relevant knowledge, make notes.

What’s on the table today?

This article will attempt to discuss the storage and calculation of Numbers in modern computers, and it is bound that, although the title is “Numbers in JavaScript”, a large portion of the paper should focus on the way Numbers are handled primarily in programming languages. Once you understand the principle, it’s much easier to master numbers built around the same principle in various languages. This, of course, includes JavaScript.

Let’s start with a few questions:

  1. Why do JavaScript numbers have them0and0?
  2. In JavaScriptNaNWhy are they not equal?
  3. Is there really only one type of numbers in JavaScript?
  4. JavaScript is often criticized0.3-0.2 == 0.1What’s the reason?
  5. What is the maximum length of the array? Why this value?
  6. Do the above problems only exist in JavaScript?

Computers are so ubiquitous these days that I’m sure even non-programmers understand that the world of computers consists of zeros and ones. And a programmer should know: 0/1 is called machine code. It has source code, inverse code, complement code, etc. A JS programmer should know that numbers in JS are not typed, i.e. there are no byte/int/float/double differences. And a slightly more research ES specification of JS programmer should know: JS number is 64 – bits under the IEEE 754 standard double precision numerical, and ES were ToInteger ToInt32 / ToUint32 / ToUint16 Type Conversion. So let’s try to talk about these.

From a hardware point of view, it is relatively easy to maintain two states, such as a diode on or off, and an electrical pulse high or low, thus making it simpler and more efficient to implement integrated circuits, so computers generally use 0 and 1 for storage and calculation. So, with only 0 and 1, how do I get 1234567890? This involves machine code and truth values.

Machine code and truth value

Machine code is the binary form of integers in a computer. The rule is very simple, the highest bit of machine code (the first left bit) represents the positive and negative numbers, 0 represents a positive number, 1 represents a negative number, and the remaining bits represent specific numbers according to the rules of base conversion.

The truth value is the actual integer with positive and negative values restored by machine code according to the above conversion rules.

For example, if 8-bits is used to represent an integer, the decimal integer +6 can be expressed as 00000110. The decimal number -5 can be expressed as 10000101. Here +6 and -5 are the truth values, and the binary numbers that represent them are machine code. Again, note that the highest bit is only used to represent positive and negative, for example 10000101 has a truth value of -5 rather than 133, and that our discussion of machine code and truth values is based on the integer range, and that floating point numbers are stored in computers with very different values from integers, which will be discussed separately.

With machine code, we can use machine code in a computer to store and compute truth values. How does machine code compute in a computer?

Source code, inverse code, complement code

Machine code is divided into a variety of, mainly including source code, inverse code, complement code, shift code, etc. Today we mainly summarize the first three, and shift code is very simple, and more for comparison, do not elaborate. It should also be added that we distinguish the various forms of machine code here mainly for signed numbers, and unsigned numbers do not need to use the highest bit to represent positive and negative, so there is no need for so many encoding methods.

The original code:

The highest bits represent plus and minus, and the other bits represent the absolute value of the truth value. The highest bit 0 represents a positive number or 0, and 1 represents a negative number.

For example, the source code for +7 is 0000 0111 and that for -7 is 10000111. In the future, we’ll express it like this:

[+7] = [00000111] Original [-7] = [10000111] OriginalCopy the code

Obviously, the source code of 8-bits can record the range of [-127,+127].

The advantage of the original code is that it is easy to understand, relatively intuitive, convenient for human brain recognition and calculation.

For the source code, the human brain uses it and can directly calculate its truth value and then proceed with it. But for the computer, first of all, because the highest bit is used to represent positive and negative, so can not directly participate in the operation, need to identify and do special processing; Secondly, the specific calculation uses absolute values to operate, so the similarities and differences between the positive and negative of two operands will affect the operator. For example, the addition of two different signs actually requires the subtraction operation, and even the subtraction of two different signs requires the judgment of the absolute value to determine the result of positive and negative. As a result, the design of our computer algorithms will become extremely complex. Below, we will see how to use inverse and complement to involve sign bits in operations so that addition and subtraction can be uniformly simple and efficient, which is why inverse and complement come into being.

Radix-minus-one complement:

The inverse of a positive number is equal to its source code, while the inverse of a negative number is the result of changing the sign bits of its source code and negating the other bits one by one.

For example, if the same 8-bits string represents +7, then we have the following:

[+7] = [00000111] Original = [00000111] Inverse [-7] = [10000111] Original = [11111000] InverseCopy the code

Similarly, 8-bits can record the range of [-127,+127].

After reversing the bitwise, we can have the following operations:

2-3 = 2 + (-3) = [00000010] Original + [10000011] Original = [00000010] Inverse + [11111100] Inverse = [11111110] Inverse = [10000001] Original = -1Copy the code

Above, we convert subtraction to addition through inverse code, so that our operation will be much easier, but there are also some problems in the way of inverse code:

3-3 = 3 + (-3) = [00000011] Original + [10000011] Original = [00000011] Inverse + [11111100] Inverse = [11111111] Inverse = [10000000] Original = -0Copy the code

So minus 0, this value is meaningless. In addition, according to the inverse addition rule, if the highest bit has a carry, need to +1 in the lowest, then it will appear:

3 -2 = 3 + (-2) = [00000011] original + [10000010] Original = [00000011] inverse + [11111101] inverse +1) = [00000001] = [00000001] = 1Copy the code

This situation increases the complexity of inverse code operation and affects the efficiency. In order to solve the above problems, complement code appears.

Complement:

The inverse of a positive number is equal to its original code, while the complement of a negative number is the result of adding one to the end of its inverse.

For example, if a string of 8-bits is used to represent +7, we have the following:

[+7] = [00000111] Original = [00000111] Inverse = [00000111] Complement [-7] = [10000111] Original = [11111000] Inverse = [11111001] ComplementCopy the code

Using the complement, continue with the previous operation:

2-3 = 2 + (-3) = [00000010] Original + [10000011] Original = [00000010] Inverse + [11111100] Inverse = [00000010] Complement + [11111101] Complement = [11111111] Complement = [11111110] inverse = [10000001] original = -1Copy the code

So what if it’s 3 minus 3?

3-3 = 3 + (-3) = [00000011] Original + [10000011] Original = [00000011] Inverse + [11111100] Inverse = [00000011] Complement + [11111101] Complement = [00000000] Complement = [00000000] = 0Copy the code

Do you need to do additional addition?

3-2 = 3 + (-2) = [00000011] Original + [10000010] Original = [00000011] Inverse + [11111101] Inverse = [00000011] Complement + [11111110] Complement = [00000001] Complement = [00000001] the original = 1Copy the code

In this way, we can perfectly unify subtraction over addition, and do not need tedious positive and negative judgment, carry control, and even save a place. So, what about this position, which is 10000000? According to the regulations, 10000000 is used to represent -128, the complement/inverse/source code of the positive number is the same, and the complement of the negative number is only occupied by the [10000000] original of -0 and the [11111111] inverse transformation of the [10000000] complement represents -128, but this is just to help understand, You can’t push back to get the source and complement of -128.

Therefore, the range that the 8bits complement can record is: [-128,+127].

So far, we have seen the main way that computers store and calculate integers. Given that modern computers use complementary codes, it is easy to understand the range of representation of various numeric types. For example, the range of 32bits int is: [-231,231-1]. This will be important to understand some of the extreme situations in JavaScript later on.

To add:

We may think that the original code is easy to accept, but what kind of logic or mathematical principles are the inverse and complement based on? Here, we can discuss it briefly, because this tread is a bit beyond today’s topic.

A common example to illustrate this principle is the clock. The clock has 12 digits in a week. What if we want to adjust from 3 to 8? We could go forward plus 5, or we could go back minus 7. The relationship between the two numbers, +5 and -7, is that they both get the same result when they take the remainder of the number 12. The strict concept is the congruence that we learn when we’re young, the exact description is that the relationship above is +5 and -7 congruence to modulo 12, +5 and -7 are complementary, they complement each other. We can see that within the range of the number of the modulus, we subtract a number, which is exactly the same thing as adding the complement of that number and then mod it. The detailed process requires rigorous scientific proof. There’s a lot of literature on the web, so let’s stop there and Google it if you’re interested.

The IEEE 754 standard

As a JavaScript programmer, we only have one Number, so we got used to it from the start:

1
2
var num1 = 123;
var num2 = 1.23;
Copy the code

However, did you know that JS number is an IEEE 754 standard 64-bits double value? What kind of criteria is that? What does it mean to use this standard 64-bits double? So, to master numbers in JavaScript, we first need to understand the IEEE 754 standard. Below, I’ll try to illustrate this standard, paving the way for our final study of numbers in JavaScript.

The rationale for the standard:

We know that for a computer, there is no difference between numbers and integers, that is, there is no decimal point in a computer. From the previous discussion, we have found a perfect solution for storing computation with integers, but when it comes to decimals, we can easily find that the existing solution does not meet our needs. Computer scientists then tried several schemes, mainly fixed-point and floating-point.

The so-called fixed point number refers to a specific position where the decimal point is fixed in the middle of the number string, and both sides of the point are the integer and decimal part of the number respectively. For example, if the decimal point is fixed in the middle of an 8-bit string, 11001001 and 00110101 represent 1100.1001 and 11.0101 respectively. This scheme is simple, intuitive and easy to understand, but there is a serious waste of space, as well as easy to overflow problems.

Floating-point numbers, in which the position of the decimal point is not fixed, represent different numbers by scientific notation that controls the position of the decimal point. This representation scheme is used by the IEEE 754 standard. IEEE 754 is the most widely used standard for floating-point arithmetic. We will focus on this scheme below.

Now, let’s think about the scientific notation we learned when we were young. For example, the number -123.456 would translate into scientific notation: -1.23456 × 10^2. It already contains the major elements of the IEEE 754 standard. The first one, of course, is the plus and minus sign problem, which requires a sign; Then, you need a specific number that represents either a significant number or precision, such as 1.23456 in the example above; Then, need a digital control point position, example above 10 ^ 2, recall that we learn scientific notation, requirements at the front of the number of absolute value is greater than 1 and less than 10, who is a Base of less than 10 ^ 2 (Base), fixed Base, Base should be fixed, so here is the index and decisive, That’s 2 in the previous example. So, with these three elements, we can easily represent a number, and adjust the position of the decimal point to control the number of positive and negative, precision and size.

The factors above are converted into standard language description, we call the Sign that represents positive and negative, the number that represents precision is Mantissa or significant number, and the index that controls the position of the decimal point is called the Exponent. The index and Base work together in the calculation. The fraction field is the same as the significant number field above. The fraction field is the same as the significant number field above.

With the basics understood, let’s take a look at what the IEEE 754 standard does.

The first thing to do is to specify the number of digits occupied by these three elements in a number string. Just think about it, if the number of digits in each realization is uncertain, is it difficult for us to correctly restore the original number? The IEEE 754 standard specifies four ways to represent floating point values: single precision (32 bits), double precision (64 bits), extended single precision (43 bits above, rarely used), and extended double precision (79 bits above, usually implemented as 80 bits). Only 32-bit mode is mandatory, the rest is optional. And now the mainstream language, most of the implementation of single precision and double precision, we mainly compare the two, the figure is their respective parts of the figure, the number of bits used is as follows:

In addition, both scientific notation and standard specification require significant digits (regardless of sign bits) to be >= 1&&. Therefore, the significant digit is actually a fixed point number, the position of the decimal point is fixed between the highest and second highest digit of the significant digit field. So, according to the above rules, in binary, the highest bit can only be 1, so the standard requires that the highest bit be omitted, thereby increasing the accuracy by one bit. For example, the 32-bits single-precision significant digit range is only 23 bits, but the precision is 24 bits. 64-bits double precision, with 52 -bit significant field but 54 -bit precision.

And then there’s the question, if we follow the convention, is it impossible to represent real numbers less than 1? Because, the exponent must be greater than =0, and the significant digit must be greater than 1. Therefore, IEEE 754 standard proposed a very important exponential offset value. It says that the coded value of the Exponent field (the area occupied by the Exponent) is the actual value of the Exponent plus some fixed value. In other words, if the Exponent we calculated from the Exponent field is N, then the Exponent involved in calculating the actual floating-point number should be an n-exponent offset. According to the IEEE 754 standard, this fixed value is 2^(E-1) -1, where e is the length of the bits storing the exponent. For example, we can see from the figure above that 32-bits single-precision is 8-bits for an exponential field, so the offset would be 2^(8-1) -1 = 128−1 = 127. Thus, it is easy to conclude that the actual exponential part of a single-precision floating-point number is [-127,128]. For example, if the 32-bit single-precision index is 1 in decimal, the encoding of the index field should be 10000001. If the 32-bit single-precision index is 00000001, the actual value of the index field should be -126 in decimal. In this way, we can convert a positive exponent to a negative exponent by an offset value, bringing the floating point number close to zero. The exponent calculation of floating-point numbers is the reverse of the machine code discussed earlier, with positive numbers having the highest digit of 1 and negative numbers having the highest digit of 0.

The above description is the most important part of the IEEE 754 standard, but as a widely used industry standard, it is not enough to specify.

To add:

The IEEE 754 standard is described by Wikipedia as follows:

The standard defines formats for representing floating-point numbers (including negative zero-0 and denormal number), special values (Inf and NaN), and “floating-point operators” for these numbers. It also specifies four value rounding rules and five exceptions (including when and how exceptions are handled).

Here are a few additions that I think are relevant to the subsequent discussion of this article and may help you understand the definition of extreme phenomena:

Floating-point in specification form: If the encoded value of the exponent part of a float is between 0 < exponent < 2^(e-1) and the most significant bit (that is, the integer number) of the manta part is 1, then the float is said to be in specification form. That is, coded exactly as we described above.

Unspecified floating point: if the encoding value of the exponential part of a floating point is 0 and the mantissa is non-zero, the floating point is said to be unspecified. The IEEE 754 standard states that the exponential offset of a non-canonical floating point number is 1 greater than that of a canonical floating point number. For example, the smallest specification form of a single-precision floating-point number has an exponential partially encoded value of 1, and the actual value of the exponent is -126; The non-canonical single-precision floating-point number has an exponent field encoding value of 0, and the corresponding exponent is actually -126 rather than -127. In fact, unspecified floating-point numbers are still valid, but their absolute values are less than the absolute values of all specified floating-point numbers. That is, all non-spec floating-point numbers are closer to zero than spec floating-point numbers. The mantissa of a specification floating-point number is greater than or equal to 1 and less than 2, whereas the mantissa of a non-specification floating-point number is less than 1 and greater than 0.

The two concepts above, taken almost straight from Wikipedia, were the emergence of unstructured floating-point figures to avoid abrupt underflow and to adopt gradual downflows. This was in the 1970s, almost twice my age. Here are some very extreme cases, and I’m going to try to describe as simply as possible what it means to have floating point numbers in non-canonical form: Now, using single-precision as an example, if there are no unspecified floating-point numbers, then the difference between the two adjacent floating-point numbers with the smallest absolute values will be 2^23 times that of the floating-point number with the smallest absolute values. What is the value of the number with the smallest absolute values minus the number with the smallest absolute values?

1.00... 01 × 2^(-126) -1.00... 00 × 2^(-126) = 0.00.. 01 × 2^(-126) = 1 × 2^(-126-23) = 2^(-149)Copy the code

Obviously, the specification number with the smallest absolute value cannot express the difference between it and the next-smallest specification number, so it is easy to overspill the difference between several numbers, possibly triggering unintended consequences. If, on the other hand, an unspecified floating-point number has all zeros, the offset is 1 greater than the offset (-126 is 1 greater than -127), and the mantras are less than 1, then the minimum that an unspecified number can express is:

0.00.. 01 × 2^(-126) = 1 × 2^(-126-23) = 2^(-149)Copy the code

Therefore, the non-conventional floating point solved the aforementioned abrupt underflow and was adopted by the standard.

The IEEE 754 standard also specifies three special values:

  1. If the exponent is all 0 and the mantissa decimal part is all 0, the number is plus or minus 0. (Sign bit determines plus and minus)
  2. The exponent is 2e – 1 with all the decimal parts of the mantissa zeros, which is plus or minus infinity. (Sign bit determines plus and minus)
  3. The exponent is 2e — 1 with a non-zero mantissa, which is NaN.

Combined with the previous specified number, non-specified number and three special values, it can be summarized as follows:

Now, let’s recall the range of double-precision floating point numbers commonly described in various languages: [-1.7 × 10-308,1.7 × 10308]. To take a side step and imagine a decimal number with more than 300 digits in it is far beyond the bounds of ordinary people’s imagination. Why is this range this range? I think it should be clear from the above discussion why the numbers 1.7/308 appear.

First, it should be easy to derive the formula for a double-precision floating-point number from the offset:

Then, taking the positive number as an example, according to the convention of ±∞ and NaN in the above special values, the maximum value of the exponent should satisfy the maximum value of the exponential range of the specified number, and then the maximum value of the fractional part, it can be concluded that the binary number should be:

1
0 11111111110 11.. 11 (52)Copy the code

Convert to hexadecimal representation:

Then, according to the principle of the specification number, the inverse encoding is decimal: 1.7976931348623157 x 10^308. Similarly, if the Sign bit is reversed, it is the lower limit of the range.

So far, I am on the IEEE 754 standard is a few days to learn, say more on the misleading. Through the study of these days, I feel that we should pay special attention to the concept of precision and range when we understand IEEE 754 standard and floating point numbers. Range is just a fuzzy boundary, precision is an accurate number.

Back to the JavaScript

In the above discussion, we haven’t mentioned JavaScript much, which may seem like a departure from today’s topic, but once we understand the principles mentioned above, we’ll be able to learn more about numbers in JavaScript. This will ultimately be a discussion with more foreshadows than text, more appetizers than meals. Well, say hello, guys! Dinner’s starting!

ES’s “The Number Type” :

Now, we open up The ES specification “The Number Type” is basically read down? Such as:

The Number type has exactly 18437736874454810627 (that is, 264 − 253 + 3) values…

Why is that number? Since we say that numbers in JavaScript are 64-bits doubles, there are first possible combinations of 2^64, and then, as part of the special values in the aforementioned IEEE 754 standard, NaN and ±∞ take up 2^53 values, but represent three intuitive quantities, so add and subtract, Values 18437736874454810627 that is, 2^64 − 2^53 + 3

… The 9007199254740990 (that is, 253−2) DISTINCT “not-a-number” values…

Why so many Nans? Also, as part of the special values in the aforementioned IEEE 754 standard, NaN uses all the possibilities that the Significand is non-zero, the exponent is a specific 2^ E-1 and Sign is not required, that is, 2^53 minus ±∞.

… E is an INTEGER ranging from −1074 to 971…

Why is the exponent in this range? Instead of minus 1022 to plus 1022? In contrast to the 64-bits formula we demonstrated earlier, we used 1.m in the demonstration of mantissa according to IEEE 754 standard, while M is used in the ES specification. Of course, there is a difference in bit length in mantissa field.

And that’s pretty much the end of the numbers. The first few questions, I believe that read here students, can have the answer. However, one more question remains, is there really only one type of numbers in JavaScript? And seems to be a little bit out of step with our original intention. However, many things in the world are often like this, explaining the principle of thirsty mouth, and using the principle to explain the phenomenon is only a few words.

JavaScript isn’t just 64-bits double

Yes, the subtitle already answers our question, JavaScript isn’t just 64-bits double. We said throughout the various figures in the JavaScript, has been in accordance with the 64 – bits of double precision to describe, but, as said before, ES of ToInteger ToInt32 / ToUint32 / ToUint16 Type Conversion. These Type conversions are not apis we call directly, but are done for us by the language engine when certain operations are performed. This “stealth operation” only manifests itself in extreme cases. Now, we can go to “ToInt32″/” ToUint32 “/” ToInt16 “and compare them a little. We can see that the differences are very small, only in certain steps. For example, the difference between ToUint32 and ToUint16 is only the last step of the operation.

Let int32bit be posInt modulo 232; that is, a finite integer value k of Number type with positive sign and less than 232 in magnitude such that the mathematical difference of posInt and k is mathematically an integer multiple of 232.

Return int32bit.

Let int16bit be posInt modulo 216; that is, a finite integer value k of Number type with positive sign and less than 216 in magnitude such that the mathematical difference of posInt and k is mathematically an integer multiple of 216.

Return int16bit.

The difference is only 2^32 and 2^16. The key point is when modulo is used. According to the principle we discussed earlier, it is easy to understand that this operation determines the maximum number that can occur. Such comparison, has one advantage, can improve our speed of reading standards, and deepen the understanding, to master the standards are very helpful.

To summarize the scope of these three operations:

The ToInt32 range is [-231, -231 — 1] in other strongly typed languages.

The range of ToUint32 is [0, -232 — 1] in other strongly typed languages.

The range of ToUint16 is [0, -216 — 1] in other strongly typed languages.

By searching, you can easily find JavaScript operations that use the related operations described above. ToUint16 is only used in String.fromCharcode, which we won’t discuss. ToInt32 is used in multiple bit operators, such as ~ / << / >>, and also in parseInt. However, the use of ToUint32 appears in a large number of places, mainly distributed in the two areas of array related operations and bit operations.

With these uses of ToUint32, let’s go back to where we left off:

First, go here >>> and see the operation as follows:

1.Let lref be the result of evaluating ShiftExpression.

2.Let lval be GetValue(lref).

3.Let rref be the result of evaluating AdditiveExpression.

4.Let rval be GetValue(rref).

5.Let lnum be ToUint32(lval).

6.Let rnum be ToUint32(rval).

7.Let shiftCount be the result of masking out all but the least significant 5 bits of rnum, that is, compute rnum & 0x1F. Return the result of performing a zero-filling right shift of lnum by shiftCount bits. Vacated bits are filled with zero. The result is an unsigned 32-bit integer.

New Array (len) :

If the argument len is a Number and ToUint32(len) is equal to len, then the length property of the newly constructed object is set to ToUint32(len). If the argument len is a Number and ToUint32(len) is not equal to len, a RangeError exception is thrown.

The value returned by >>> is identical to array.length. The value returned by >>> must be a valid array.length. Explaining principles is always so complicated, but explaining phenomena by principles is always so simple.

The above.