Working with Numbers

Overview

Virtually all programming languages can be used to write programs that allow users to work with numeric values and carry out mathematical operations with varying degrees of complexity, and JavaScript is no exception. There are two important built-in objects in the JavaScript language that facilitate working with numbers - the Math object and the Number object. We will be looking at the properties and methods offered by both of these objects - and how to use them - in this article.

As we have stated elsewhere, JavaScript is considered to be a loosely typed language, which amongst other things means that the type of a variable does not have to be declared. The variable's type is assumed by JavaScript based on the value assigned to it when it is initialised. The variable's type can also change. For example, we can assign a numeric value to a variable initially, and later assign a non-numeric value to the same variable without JavaScript complaining.

This makes JavaScript far more flexible than a strongly-typed programming language, but that flexibility comes at a cost. Because there are virtually no restrictions when it comes to data typing, we need to exercise a lot more care when writing code because there are none of the checks and balances associated with a strongly-typed language like Java. Converting a variable from a string to a number, or vice versa, is often done implicitly, depending on the kind of operation being carried out and the nature of the variable, or variables, involved.

A function that expects a numeric value as one of its arguments will not cause a script to crash if it instead receives a string value or a Boolean value. On the other hand, it may not behave quite as we expected either, and we can demonstrate this with an example. The following code generates a web page with a form that allows the user to enter two numeric variables:

<!doctype html>
<html lang="en">

  <head>
    <meta charset="utf-8">
    <title>JavaScript Demo 65</title>
    <style>
      caption { font-weight: bold; }
      form {
        width: max-content;
        margin: 1em auto;
        padding: 0.25em;
        border: 1px solid black;
      }
      #output { border: 1px solid black; }
      table { padding: 0.5em; }
      td {
        padding: 0.5em;
        margin: 0.5em;
      }
    </style>
  </head>

  <body>

    <form id="frm">
      <table>
        <caption>Calculate Values</caption>
        <tr>
          <td><label>Variable 1: <input type="number" name="var1"></label></td>
        </tr>
        <tr>
          <td><label>Variable 2: <input type="number" name="var2"></label></td>
        </tr>
        <tr>
          <td><button name="add">Add variables</button></td>
        </tr>
        <tr>
          <td><button name="mul">Multiply variables</button></td>
        </tr>
        <tr>
          <td id="output"><label>Result: <output name="out"></output></label></td>
        </tr>
      </table>
    </form>

    <script>
      const frm = document.getElementById("frm");

      frm.add.addEventListener("click", calc);
      frm.mul.addEventListener("click", calc);

      function calc() {
        event.preventDefault();
        let a = frm.var1.value;
        let b = frm.var2.value;
        if ( event.target === frm.add ) {
          result = a + b;
        }
        else if ( event.target === frm.mul ) {
          result = a * b;
        }
        frm.out.innerHTML = result;
      }
    </script>

  </body>
</html>

Copy and paste this code into a new file in your HTML editor, save the file as javascript-demo-65.html, and open the file in a web browser. You should see something like the illustration below. Try entering different values - both numeric and non-numeric - into the form's input fields, and click the "Add variables" and "Multiply variables" buttons to see what happens. Depending on the values you input and which button you click, you should see something like the illustration below.


The add() function concatenates the input values

The add() function concatenates the input values


In the above example, we entered values of 7 and 3 and clicked on the "Add variables" button. You may have seen in the HTML code that both of the <input> elements have their type attribute set to number, so we would expect the add() function to add the two numbers together to produce a result of 10. Instead, we see the result 73. Instead of adding the numbers together, the function has concatenated them! The reason for this is that all <input> elements return a string, even if their type attribute is set to number.

One of the slightly confusing features of JavaScript is that the plus sign (+) is both the mathematical operator that represents addition and the string concatenation operator. As far as JavaScript is concerned, the add() function has received two string values as input and has therefore concatenated them, resulting in a string value of "73" being displayed in the <output> field rather than the sum of the two numeric variables we have entered.

However, if we enter the same two values and click on the "Multiply variables" button, the result is exactly what we intended - an output value of 21. That is because JavaScript sees the multiplication operator and assumes that the input values are numeric, so it multiplies them together.

Note that, depending on which browser you are using, the input fields may or may not accept a non-numeric string value as input. The end result of attempting to do so will be the same, however. Even if the browser does allow you to enter non-numeric values, they will effectively be interpreted as having a value of zero.

We can of course force JavaScript to convert a string representing a numeric value into a numeric value, and there is (as is usually the case with JavaScript) more than one way to achieve this. We could, for example replace this line of code:

result = a + b;

with this:

result = parseInt(a) + parseInt(b);

This code is perfectly OK if we are entering only integer (whole number) values, but the result will be inexact for floating-point values, because the parseInt() function truncates any floating-point number it encounters by removing the fractional part, leaving only the integer part. A better option is to use either the parseFloat() function or the unary addition operator (+). Either of the following statements would provide a far more precise result:

result = +a + +b;

result = parseFloat(a) + parseFloat(b);

We still need to exercise caution, however. If we enter fractional values value into the input boxes, both of the above statements can return a result that is not quite right. Let's look at an example. Suppose we enter the values 7.911 and 23.7 in the "Variable 1" and "Variable 2" input fields. You can probably do the addition in your head, which would give you a result of 31.611. However, using either of the statements above produces a result of 31.610999999999997!


Floating-point errors can occur in JavaScript

Floating-point errors can occur in JavaScript


In many applications, this discrepancy is not problematic. The degree of error is miniscule, and we can use the JavaScript Number object's toFixed() method (more about that in due course) to tidy things up and display the correct result. The problem occurs because computers store decimal numbers internally as binary numbers. This works perfectly well for integer values, but many decimal fractions have no exact binary equivalent, so floating-point numbers are stored as close approximations.

In most programming languages this is not such an obvious problem because those languages offer numerical data types with varying degrees of precision. Both Java and C++ have the int (integer), float (single-precision floating point), and double (double-precision floating point). JavaScript has only one numeric data type - all numeric values are stored as 64-bit double precision floating point values.

For most applications, it's relatively easy to write code that handles mathematical calculations correctly, and produces the results we are expecting, using the methods provided by the Math and Number objects. For each mathematical operation to be carried out, we should clearly define the nature of the input and output values involved, and devise suitable test cases to ensure that our code is behaving in accordance with the application's requirements.

Representing numbers

As we have already mentioned, all numeric values in JavaScript are stored as 64-bit double precision floating point numbers. The numbers are stored in their binary representation in accordance with the IEEE 754 standard double-precision binary floating-point format, which is officially referred to as binary64. A binary64 number is structured as shown in the illustration below.


The structure of a binary64 number

The structure of a binary64 number


The sign bit, as the name suggests, represents the sign of the number (positive = 0, negative = 1), even if the number is zero. The 11-bit exponent field has an unsigned integer value in biased form, in the range 0 to 2047. The term biased form means that the number is offset from the number actually being represented by another number called the exponent bias, which in this case is a zero offset of 1023.

The exponent is stored in this way because it must be able to represent both positive and negative values, but the usual method of representing signed numbers (two's complement) would make binary comparisons more difficult. Putting it another way, an unsigned exponent expressed as a regular binary number is simply easier to work with than an exponent that is represented using two's complement.

The actual exponent is derived by subtracting the exponent bias from the (unsigned) biased exponent. Thus, zero is represented by the biased exponent 1023. The exponent values that can be represented range from -1022 to +1023. The values -1023 (all bits set to 0) and +1024 (all bits set to 1) are reserved for special numbers.

The 53-bit significand consists of the 52 bits in the significand field (you might also see this called the fraction, coefficient, argument, mantissa, or characteristic) prefixed by an implicit binary 1 (sometimes called the "hidden" bit), followed by a binary point (the binary equivalent of the decimal point).

The 11-bit width of the exponent in a binary64 floating point number allows numeric values (positive or negative) with a magnitude of between 1.7976931348623157e-308 and 1.7976931348623157e+308 to be represented, with a precision of around 17 decimal digits. The formula for extracting the value of a number N stored in binary64 format is:

N  =  (-1) sign × (1.b51 b50 b49  . . . b0 )2  × 2 e-1023

The first part of this formula looks rather strange, but it simply produces a value of +1 or -1 by raising -1 to the power of the sign bit, which is either 1 (for a negative value) or 0 (for a positive value):

-1 1  =  -1
-1 0  =  +1

The absolute value derived by multiplying the significand by 2 e-1023 is then multiplied by this value to give a positive or negative result.

The largest value that can be stored in the 53-bit significand (including the "hidden" bit) is:

1.1111111111111111111111111111111111111111111111111111

So how do we convert a decimal (base-10) number to binary64 format? Determining the sign bit is of course a trivial matter, since it will be either 0 or 1, depending on whether the number is positive or negative. Determining the binary values of the exponent and the significand is a little trickier. Let's start by looking at how to convert a decimal integer to binary64. We'll use the number 36510  as an example.

Converting 36510  to binary, we get 101101101. In order to get this value into the required format for the significand, we need to shift the binary point to the left until we have only a single binary one to the left of the binary point. In this case, that means shifting the binary point eight places to the left, resulting in a binary value of 1.01101101. So far so good, but how do we find the value of the exponent?

Actually, it's fairly straightforward. Because we had to shift the binary representation of 36510  eight places to the left to get the significand in the required format, we have to multiply the significand by 2 8 in order to restore it to its original value. Remember, however, that the exponent is expressed in biased form with a zero offset of 1023. Our exponent will therefore be the binary equivalent of 8 + 1023, so the calculation is as follows:

810  + 102310   =  103110   =  100000001112 

The binary64 representation of 36510  will therefore comprise the elements shown below. Note that the presence of the leading binary 1 (the so-called "hidden" bit) and the binary point that follows it are implied. Only the bits that follow the binary point in the significand are stored in its binary64 representation.

Sign bit: 0
Exponent: 10000000111
Significand: 0110110100000000000000000000000000000000000000000000

If the number we want to convert is a fraction, or has a fractional component, we can follow the same procedure. As an example, let's suppose we want to convert the decimal value 57.89 to its binary64 representation. We start by converting 57.8910  to its binary equivalent:

111001.11100011110101110000101000111101011100001010010

The six binary digits to the left of the binary point represent the value 5710  exactly. The binary digits to the right of the binary point represent the fractional part of the number (0.8910 ) to 47 significant binary digits, which is the closest approximation possible.

In fact, there is no exact binary representation for the decimal fraction 0.8910 , even if we had an infinite number of binary digits available after the binary point. The bit-pattern that extends from the 3rd position after the binary point to the 22nd position after the binary point - 10001111010111000010 - will simply repeat itself forever.

Note that, due to rounding, the final two digits in the significand are 10 rather than 01. The general rule when rounding binary fractions to n places after the binary point is that, if the digit that would otherwise follow the nth-placed digit is a binary 1 (which is the case here), then the number should be rounded up.

In order to convert the binary representation of 57.8910  into a valid binary64 significand, we need to move the binary point five places to the left, so our exponent will be the binary equivalent of 5 + 1023. We now have enough information to be able to write the binary64 representation of 57.89:

Sign bit: 0
Exponent: 10000000100
Significand: 1100111100011110101110000101000111101011100001010010

You will rarely if ever need to convert a decimal number into its binary64 representation. If for some reason you do have to do this, there are online resources available for that purpose, such as the Online Binary-Decimal Converter utility provided by François Grondin. It is however important to understand the way in which JavaScript and other languages store double-precision floating point numbers, if only to gain an awareness of the limits imposed by the binary64 format on the precision with which numeric values can be stored.

Integers and the BigInt object

Because of the way numbers are stored in JavaScript, the largest integer value that can be represented safely in JavaScript is 2 53-1, or 9,007,199,254,740,991, because we only have 53 bits available for storing the significand (including the "hidden" bit, which normally has a value of 1). This number is represented in binary64 format as follows:

Sign bit: 0
Exponent: 10000110011
Significand: 1111111111111111111111111111111111111111111111111111

We could of course represent larger integer values by increasing the size of the exponent, but since we can't increase the number of bits available for the significand, this represents a loss of precision. For example, for numbers in the range 2 53 - 2 54, only even numbers will be accurately represented, as demonstrated below. There is thus no reliable way to represent any integer value greater than 2 53-1 in the binary64 format.

console.log(9007199254740992); // 9007199254740992
console.log(9007199254740993); // 9007199254740992
console.log(9007199254740994); // 9007199254740994
console.log(9007199254740995); // 9007199254740996
console.log(9007199254740996); // 9007199254740996
console.log(9007199254740997); // 9007199254740996
console.log(9007199254740998); // 9007199254740998
console.log(9007199254740999); // 9007199254741000
console.log(9007199254741000); // 9007199254741000
console.log(9007199254741001); // 9007199254741000
console.log(9007199254741002); // 9007199254741002

The largest positive and negative safe integer values can be accessed using two special properties of the JavaScript Number object - the constants: Number.MAX_SAFE_INTEGER and Number.MIN_SAFE_INTEGER, which represent the values 9,007,199,254,740,991 and -9,007,199,254,740,991 respectively.

Does this mean we can't express or use integer values with an absolute value greater than 2 53-1? Actually, the answer is no. JavaScript provides the built-in BigInt object for this purpose. A bigint primitive can represent integer values too large to be represented by the number primitive, and can be created by appending "n" to the end of an integer literal. For example:

let x = 9007199254740991n;
let y = x + 10n;
console.log(x); // 9007199254740991n
console.log(y); // 9007199254741001n
console.log(typeof(x)); // bigint
console.log(typeof(y)); // bigint

We can also create a BigInt value using the BigInt() constructor (without the new keyword):

let x = BigInt(9007199254740991);
let y = x * x;
console.log(x); // 9007199254740991n
console.log(y); // 81129638414606663681390495662081n
console.log(typeof(x)); // bigint
console.log(typeof(y)); // bigint

We don't use the new keyword with the BigInt() constructor because it returns a bigint primitive rather than an object, and is therefore not considered to be a constructor method as such. You should also exercise caution when using the BigInt() method to coerce an arbitrarily large integer value to a BigInt value, because loss of precision can occur. For example:

console.log(BigInt(12345678901234567890)); // 12345678901234567168n

You can avoid this problem by putting the numeric argument inside quotes, like this:

console.log(BigInt("12345678901234567890")); // 12345678901234567890n

From the above, we can see that we can represent integer values that are greater in magnitude than MAX_SAFE_INTEGER and MIN_SAFE_INTEGER. We can also use these values together with JavaScript's arithmetic operators to carry out calculations. There are a couple of things to note, however. The first is that all values used in a calculation must be of type bigint. As we saw above, the following code works:

let x = 9007199254740991n;
let y = x + 10n;
console.log(y); // 9007199254741001n

The following code does not work as expected:

let x = 9007199254740991n;
let y = x + 10;
console.log(y); // Uncaught TypeError: can't convert BigInt to number

The only difference between these two code snippets is that we have omitted the "n" from the end of the numeric literal "10" in the second line, so JavaScript throws a type error. The second thing to note is that BigInt values cannot be used with the built-in methods provided by the Math object. You should also be aware that, even though it is possible to coerce Number values to BigInt values and vice versa, the precision of a BigInt value may be lost when it is coerced to a Number value.

Most of JavaScript's operators support the bigint type to a greater or lesser extent although, with one or two exceptions, all of the operands involved must be BigInt values. Another caveat is that calculations involving BigInt values will always return BigInt values. This means that, whereas operations involving addition, subtraction or multiplication will always produce a correct result, the result of dividing one BigInt value by another will only produce a correct result if the divisor is a factor of the dividend:

let x = 9n;
console.log(x/3n); // 3n (result is correct)
console.log(x/2n); // 4n (result rounded down to nearest BigInt value)

One advantage of the bigint type is that for some arithmetic operations involving integer values it can yield results of greater precision than the same operations carried out with integers of the number type. For example:

let num = 25 ** 30;
let big = 25n ** 30n;
console.log(num); // 8.673617379884035e+41
console.log(big); // 867361737988403547205962240695953369140625n

The size of a BigInt value is only limited by the amount of computer memory available for storing it, so even really huge integer values can be represented with no loss of precision. The largest Number value, on the other hand, is represented by the constant Number.MAX_VALUE, which represents a value of 1.7976931348623157e+308 (approximately 2 1024), which has a precision of 17 significant digits.

As we have already stated, an exponent of 1024 is reserved for special use, so the largest number we can actually store in binary64 format has an exponent of 1023, with all of the bits in the significand being set to 1. If the binary point after the first digit in the significand is shifted 52 places to the right, we will have the integer 9,007,199,254,740,991 (the value represented by MAX_SAFE_INTEGER).

Shifting the binary point a further 971 places to the right will give us the value 9,007,199,254,740,991 × 2 971 = 1.7976931348623157e+308 to 17 significant digits - the number represented by Number.MAX_VALUE. As we saw above, the fact that we only have 53 binary digits available for the significand in binary64 numbers (including the hidden bit) means that, as we increase the size of the exponent, there is an increasing loss of precision.

If we decrease the value of the significand by changing the last binary digit from a 1 to a 0, for example, the resulting binary64 number converts to a decimal value of 1.7976931348623155e+308 to 17 significant digits. At first glance this does not seem to be so very different from the value given for Number.MAX_VALUE. In fact, the difference between the two values is 2e+292 - a staggeringly large number!

We can store some very large values in binary64 format, but we will lose more and more precision as the size of the numbers increases. If we really need to carry out calculations involving such large numbers, we should consider using BigInt. As a general rule, however, BigInt should only be used if our code is required to deal with values greater than 2 53. Another thing to avoid, as we have mentioned previously, is coercion between Bigint and Number values, which can result in loss of precision.

One final thing to note is that the numeric argument passed to the BigInt() constructor method does not have to be a base-10 (decimal) value. It can also be a binary, octal or hexadecimal number, although care must be taken to append the correct prefix to the value passed as an argument. Note also that, regardless of the argument's number base, the BigInt value is stored as a base-10 number. The following code illustrates how this works:

const myBin = BigInt(0b10011010010);
const myOct = BigInt(0o2322);
const myHex = BigInt(0x4d2);

console.log(myBin); // 1234n
console.log(myOct); // 1234n
console.log(myHex); // 1234n

We can also create BigInt values by appending the "n" suffix to binary, octal or hexadecimal numbers, as the following code demonstrates:

const myBin = 0b10011010010n;
const myOct = 0o2322n;
const myHex = 0x4d2n;

console.log(myBin); // 1234n
console.log(myOct); // 1234n
console.log(myHex); // 1234n

BigInt.asIntN()

Although BigInt values can be of an arbitrary size, it is possible to set a limit on the number of bits to be used using the static BigInt.asIntN() and BigInt.asUintN() methods. The first of these, BigInt.asIntN(), takes two arguments, both of which are numeric values. The value received as the first argument specifies the maximum number of bits that can be used to store the value received as the second argument.

In the following example, the checkInput() function accepts a numeric value as input, converts it to a BigInt value, and checks to see whether that value can be stored as a 16-bit integer. If so, it returns the BigInt value. Otherwise, it returns a message indicating that the input is out of range.

const maxBits = 16;

function checkInput(input) {
  let value = BigInt(input);
  if( value === BigInt.asIntN(maxBits, value)) {
  return value;
  }
  else {
    return "Input is out of range.";
  }
}

console.log(checkInput(32767)); // 32767n
console.log(checkInput(32768)); // Input is out of range.
console.log(checkInput(-32768)); // -32768n
console.log(checkInput(-32769)); // Input is out of range.

BigInt.asUintN()

The static BigInt.asUintN() method works in a similar fashion, except that it works with unsigned inters. Whereas a 16-bit signed integer can represent values between -32768 and +32767, a 16-bit unsigned integer can represent values in the range 0 to 65535. Note however that the input to BigInt.asUintN() must also be an unsigned integer value. The following code snippet shows what happens if a negative integer value is passed to this method:

console.log(BigInt.asUintN(16, -65n)); // 65471n

As you can see, the numeric value of the BigInt value returned is the maximum value of a 16-bit unsigned integer (65535), plus 1, minus the absolute value of the input value, i.e. (65535 + 1) - 65 = 65471. If we want to use the BigInt.asUintN() method to work with negative values, we have to convert those values to their unsigned equivalents.

The code below contains the function absBigInt(), which takes a positive or negative integer value as input and either returns its absolute value as a 16-bit unsigned BigInt value or (if it requires more than 16 bits) displays a message stating that it is not in range.

const maxBits = 16;

function absBigInt(input) {
  let value = BigInt(input);
  if (value < 0) {
    value = value * -1n;
  }
  let absVal = BigInt.asUintN(maxBits, value);
  if (absVal === value) {
    return absVal;
  }
  else {
    return "Input is out of range.";
  }
}
console.log(absBigInt(+65535)); // 65535n
console.log(absBigInt(+65536)); // Input is out of range.
console.log(absBigInt(-65535)); // 65535n
console.log(absBigInt(-65536)); // Input is out of range.

Other BigInt methods

If we are creating an application that works with BigInt values we will almost certainly need to display those values on screen at some point. BigInt provides two methods that allow us to convert a BigInt value into a string value - Bigint.prototype.toString() and Bigint.prototype.toLocaleString(). Both of these methods are instance methods, which means that, unlike BigInt.asIntN() and BigInt.asUintN(), they can be called directly on a bigint primitive. For example:

let myBigInt = 1024n;
console.log(myBigInt); // 1024n
console.log(typeof myBigInt); // bigint
let myString = myBigInt.toString()
console.log(myString); // 1024
console.log(typeof myString); // string

The Bigint.prototype.toString() method takes an optional radix argument that specifies the number base used to present the numeric value it is called on. The values accepted are integer values in the range 2-36, although there will seldom if ever be a requirement to pass radix values other than 2 (binary), 8 (octal) or 16 (hexadecimal). The following code demonstrates how this works:

const myNum = 234n;

console.log(myNum.toString(2)); // 11101010
console.log(myNum.toString(8)); // 352
console.log(myNum.toString(16)); // ea

When the radix argument is not specified, it defaults to a value of 10. Note that, although the values displayed are correct binary, octal or hexadecimal representations of the value toString() is called on, the resulting string does not include the binary (0b), octal (0o), or hexadecimal (0x) prefix that is normally used to signify the base of the number represented.

If any of these strings were to be passed to a function expecting a numeric argument, such as BigInt(), they would be treated as base-10 values or (in the case of the hexadecimal value, which contain non-numeric characters) cause a syntax error. We can circumvent this problem by appending the appropriate prefix to the string created by the toString() method. For example:

const myNum = 234n;

const myBin = "0b" + myNum.toString(2);
const myOct = "0o" + myNum.toString(8);
const myHex = "0x" + myNum.toString(16);

console.log(myBin); // 0b11101010
console.log(myOct); // 0o352
console.log(myHex); // 0xea

console.log(BigInt(myBin)); // 234n
console.log(BigInt(myOct)); // 234n
console.log(BigInt(myHex)); // 234n

The BigInt.prototype.toLocaleString() method also converts a numeric value to a string, but is used when we want to present the string in a language-specific format. This method accepts two arguments, both of which are optional (although see below). The first of these is the locales argument, which consists of the ISO 639-1 language code and the ISO 3166-1 country code, separated by a hyphen. For example:

const myNum = 123456789n;

console.log(myNum.toLocaleString("de-DE")); // 123.456.789
console.log(myNum.toLocaleString("en-GB")); // 123,456,789
console.log(myNum.toLocaleString("en-IN")); // 12,34,56,789

The second argument is the options argument, which can be used to more precisely specify the output format. For example:

const myNum = 123456789n;

console.log(myNum.toLocaleString("de-DE",
  { style: "currency", currency: "EUR" })); // 123.456.789,00 €

The last BigInt method we will briefly look at here is the BigInt.prototype.valueOf() method, which is used to return the wrapped primitive value of a BigInt object. The following code demonstrates how it works:

const myObj = Object(123n);
const myBigInt = myObj.valueOf();

console.log(myObj); // BigInt { }
console.log(typeof myObj); // object
console.log(myBigInt); // 123n
console.log(typeof myBigInt); // bigint

Floating-point values

As we have previously stated, all variables of type number are stored in binary64 format. This means that the largest positive integer value that can be represented safely in JavaScript is 2 53 - 1, and the largest negative integer value that can be represented safely is -2 53 + 1. Arbitrarily large integer values can be represented as BigInt values, although care must be taken when coercing integer values of type bigint to type number and vice versa (as a general rule we should avoid coercion between these types if possible).

When it comes to floating point numbers, we can work with much larger values. Positive and negative values with magnitudes of between 2 -1022 (2.2250738585072014e-308) and 2 1024 (1.7976931348623157e+308) can be represented, with a precision of around 17 decimal digits. What this means in real terms is that we can represent both very large numbers and very small numbers which, if written in their full decimal form rather than in scientific notation, could have over 300 decimal digits. Precision, on the other hand, is limited to 16 or 17 decimal digits because we only have 53 binary digits in which to store the number's significand.

For the kind of floating-point values we generally have to deal with when writing applications, this degree of precision is usually perfectly adequate. Keep in mind, however, that although some numbers with fractional components such as 0.625, 0.5, 0.25 or 0.125 can be represented exactly in binary form, the vast majority cannot. If a fraction can be raised to some power of two to arrive at a whole number, it can be represented exactly as a binary fraction, otherwise its binary representation will be an approximation.

The fractional component of a binary floating-point number consists of some number of binary digits following the binary point, each representing 2 -n where n represents the position the binary digit occupies in relation to the binary point. Consider the binary number 1010.101. The digits to the left of the binary point represent an integer value - in this case 10102  = 1010 . The value to the right of the binary point (.101) can be evaluated as follows:

Fractional component: 2 -1 + 0 + 2 -3  =  0.5 + 0 + 0.125  =  0.625

The decimal value 10.625 can thus be represented exactly as a binary number, and has seven significant (binary) digits. Unfortunately, as we have indicated, most decimal fractions have no exact binary equivalent, so a binary representation of the fractional part of a decimal floating-point value is almost always an approximation rather than an exact representation. This would be the case even if we had an unlimited number of binary digits available after the binary point, which of course we don't.

To illustrate the point, let's consider the decimal fraction 0.1 (i.e. one tenth, or 1/10). To convert a fractional value to its binary representation, we multiply it repeatedly by two. Each time we do so, we note down the integral part, and repeat the procedure for the fractional part of the result. We keep repeating this process until the fractional part becomes zero, or until we can see a repeating pattern:

2 × 0.1 = 0.2 (0)
2 × 0.2 = 0.4 (0)
2 × 0.4 = 0.8 (0)
2 × 0.8 = 1.6 (1)
2 × 0.6 = 1.2 (1)
2 x 0.2 = 0.4 (0)
2 x 0.4 = 0.8 (0)
2 x 0.8 = 1.6 (1)
2 x 0.6 = 1.2 (1)

By now it should be fairly obvious that, as we continue the process, the sequence of results (0.4, 0.8, 1.6, 1.2) will repeat itself forever, which means that following the first binary digit in the sequence, the bit-pattern 0011 will also repeat itself forever. The binary representation of the decimal fraction 0.1 (0.0001100110011 . . . . ) will therefore always be an approximation. How good that approximation is depends on the number of bits we use to represent the binary fraction.

That brings us to another point. The total number of bits available for the significand of a binary64 double-precision value is 53, including the so-called "hidden" bit. These bits represent both the integer part and the fractional part of a floating-point number. The magnitude of that number depends on the value stored in the 11-bit exponent field, which can range from -1022 to +1023.

In order to derive the value stored in the binary64 format, the significand is multiplied by 2n, where n represents the exponent. Essentially, the exponent tells us how far to the left (in the case of a negative exponent) or to the right (for a positive exponent) we need to move the binary point in the significand in order to obtain the stored value as a binary number.

For numbers with a large integer component, more bits will be required to represent that part of the number, leaving fewer bits available to represent the fractional part, and resulting in a loss of precision. There is therefore a trade-off between the magnitude of a floating-point number and the precision to which its fractional component (assuming one exists) is represented. In truth, we are rarely if ever interested in in a fractional component when dealing with very large numbers.

Given that the exponent and the significand in a binary64 number are limited to 11 and 53 binary digits respectively, the number of different floating-point values that can be stored in this format, although very large, is finite. The limited precision of binary64 numbers means that there are gaps between consecutive values, the magnitude of which for a given range of numbers can be determined according to the size of the exponent.

To demonstrate how this works, let's consider a greatly scaled-down and simplified floating-point system in which the significand consists of five binary digits (including a "hidden" bit), and exponents can range from -2 to +2. For the purposes of this exercise, we will only concern ourselves with positive numbers, so the sign bit will always be zero. The exponent is stored in three bits with an exponent bias of 2, and can have valid values of 0002 , 0012 , 0102 , 0112  and 1002 . This system, which we'll call "Binary8", can represent eighty different numeric values, from 0.2510  to 7.7510  (0.012  to 11.112 ).



The "Binary8" number system
Binary8 FormatBase-2 CalculationBase-2 ValueBase-10 Value
000000001.0000 x 2-20.0100000.250000
000000011.0001 x 2-20.0100010.265625
000000101.0010 x 2-20.0100100.281250
000000111.0011 x 2-20.0100110.296875
000001001.0100 x 2-20.0101000.312500
000001011.0101 x 2-20.0101010.328125
000001101.0110 x 2-20.0101100.343750
000001111.0111 x 2-20.0101110.359375
000010001.1000 x 2-20.0110000.375000
000010011.1001 x 2-20.0110010.390625
000010101.1010 x 2-20.0110100.406250
000010111.1011 x 2-20.0110110.421875
000011001.1100 x 2-20.0111000.437500
000011011.1101 x 2-20.0111010.453125
000011101.1110 x 2-20.0111100.468750
000011111.1111 x 2-20.0111110.484375
000100001.0000 x 2-10.1000000.500000
000100011.0001 x 2-10.1000100.531250
000100101.0010 x 2-10.1001000.562500
000100111.0011 x 2-10.1001100.593750
000101001.0100 x 2-10.1010000.625000
000101011.0101 x 2-10.1010100.656250
000101101.0110 x 2-10.1011000.687500
000001111.0111 x 2-10.1011100.718750
000110001.1000 x 2-10.1100000.750000
000110011.1001 x 2-10.1100100.781250
000110101.1010 x 2-10.1101000.812500
000110111.1011 x 2-10.1101100.843750
000111001.1100 x 2-10.1110000.875000
000111011.1101 x 2-10.1110100.906250
000111101.1110 x 2-10.1111000.937500
000111111.1111 x 2-10.1111100.968750
001000001.0000 x 201.0000001.000000
001000011.0001 x 201.0001001.062500
001000101.0010 x 201.0010001.125000
001000111.0011 x 201.0011001.187500
001001001.0100 x 201.0100001.250000
001001011.0101 x 201.0101001.312500
001001101.0110 x 201.0110001.375000
001001111.0111 x 201.0111001.437500
001010001.1000 x 201.1000001.500000
001010011.1001 x 201.1001001.562500
001010101.1010 x 201.1010001.625000
001010111.1011 x 201.1011001.687500
001011001.1100 x 201.1100001.750000
001011011.1101 x 201.1101001.812500
001011101.1110 x 201.1110001.875000
001011111.1111 x 201.1111001.937500
001100001.0000 x 2110.0000002.000000
001100011.0001 x 2110.0010002.125000
001100101.0010 x 2110.0100002.250000
001100111.0011 x 2110.0110002.375000
001101001.0100 x 2110.1000002.500000
001101011.0101 x 2110.1010002.625000
001101101.0110 x 2110.1100002.750000
001101111.0111 x 2110.1110002.875000
001110001.1000 x 2111.0000003.000000
001110011.1001 x 2111.0010003.125000
001110101.1010 x 2111.0100003.250000
001110111.1011 x 2111.0110003.375000
001111001.1100 x 2111.1000003.500000
001111011.1101 x 2111.1010003.625000
001111101.1110 x 2111.1100003.750000
001111111.1111 x 2111.1110003.875000
010000001.0000 x 22100.0000004.000000
010000011.0001 x 22100.0100004.250000
010000101.0010 x 22100.1000004.500000
010000111.0011 x 22100.1100004.750000
010001001.0100 x 22101.0000005.000000
010001011.0101 x 22101.0100005.250000
010001101.0110 x 22101.1000005.500000
010001111.0111 x 22101.1100005.750000
010010001.1000 x 22110.0000006.000000
010010011.1001 x 22110.0100006.250000
010010101.1010 x 22110.1000006.500000
010010111.1011 x 22110.1100006.750000
010011001.1100 x 22111.0000007.000000
010011011.1101 x 22111.0100007.250000
010011101.1110 x 22111.1000007.500000
010011111.1111 x 22111.1100007.750000


The range of floating-point numbers we are able to represent with our "Binary8" system could of course be extended by increasing the number of bits in the exponent and changing the bias accordingly. We could also improve precision by increasing the number of bits in the significand. The intention here, however, is to demonstrate the limitations imposed when using a fixed-width binary format to represent base-10 floating-point values.

Looking at the table above, we can see that, like any fixed-width number format, the range of values that can be represented is discontinuous - there are significant gaps between successive values. Note also that the size of the gap between two consecutive values is the same for a given exponent. The interval between consecutive values in the range 1 × 2 -2 and 1 × 2 -1 is has a constant value of 0.0000012  (0.01562510 ). For values in the range 1 × 2 -1 and 1 × 2 0, the size of the interval is 0.000012  (0.0312510 ) - double the size of the interval in the preceding range.

We can see that, for any range of values between 1 × 2n and 1 × 2n+1, increasing the value of n by one will double the size of the interval between consecutive values within that range. Any fixed-width binary representation of floating-point values - including the binary64 double-precision floating-point format used by JavaScript and other programming languages - will suffer a similar loss of precision as the magnitude of the numbers it represents grows.

As the size of the interval between consecutive floating-point values increases, the number of intervals between successive integer values declines. In our "Binary8" scheme, there are sixteen intervals between the integer values 1 and 2. Between 2 and 3, and between 3 and 4, there are eight intervals each. Between 4 and 5 there are six intervals, and between 5 and 6 there are only four intervals.

If we extrapolate this pattern, it becomes clear that at some point we will only be able to accurately represent integer values. At some further point, we will not even be able to represent consecutive integer values. This is exactly what happens in the binary64 representation of floating-point values. With a 53-bit significand (including the "hidden" bit), all floating-point values between 2 52 and 2 53 are consecutive integer values. We are unable to represent consecutive integers greater than 2 53, which is why the Number.MAX_SAFE_INTEGER constant is set to 2 53-1.

Subnormal values

We said earlier that positive and negative values with magnitudes of between 2-1022 (2.2250738585072014e-308) and 21024 (1.7976931348623157e+308) can be represented in binary64 format. It should therefore come as no surprise that one of the properties of the Number object, which we'll be looking at shortly, is the constant Number.MAX_VALUE, which represents the largest value we can represent in JavaScript:

console.log(Number.MAX_VALUE); // 1.7976931348623157e+308

There is however another property of the Number object called Number.MIN_VALUE, which gives us the smallest positive value we can represent in JavaScript:

console.log(Number.MIN_VALUE); // 5e-324

This value is considerably smaller than 2.2250738585072014e-308, which is the smallest normalised non-zero binary64 value. What do we mean by "normalised"? When we talk about normal binary64 numbers, we assume two things.

First, we assume that the exponent field will have a minimum value of 000000000012 , which equates to -102210  when we take into account the exponent bias (102310 ). The second assumption is that the "hidden" bit will always be 1, which for normalised binary64 numbers is indeed the case. Here is the binary64 representation of 2.2250738585072014e-308:

Sign bit: 0
Exponent: 00000000001
Significand: 0000000000000000000000000000000000000000000000000000

Remember, the "hidden" bit is always set to 1 for normalised binary64 numbers. Therefore, in order to convert this binary64 representation to a base-10 number, the calculation is:

1 × 2-1022  =  2.2250738585072014e-308

Small as this value is, there is a significant gap between it and zero. If we can't represent smaller values in JavaScript, what will happen if, for example, we divide this value by two? Let's try it:

const smallFloat = -2.2250738585072014e-308;
console.log(smallFloat/2); // -1.1125369292536007e-308

Which is the correct result. Sometimes, a calculation results in a value too small to be represented as a normal binary64 number, creating a condition known as arithmetic underflow (or floating-point underflow or just underflow). The interval between zero and the smallest normal floating-point value is called the underflow gap, and is much larger than the gaps between consecutive normal floating-point values just outside the underflow gap by several orders of magnitude.

Prior to 1984, if the result of a floating-point calculation turned out to be in the underflow gap, it would typically be converted to zero, either at the hardware level or by the system software responsible for handling an underflow condition - an action known as "flushing to zero".

The 1984 version of the IEEE 754 standard (see above) introduced subnormal numbers (sometimes called denormal numbers). These subnormal numbers, which include zero, occupy the underflow gap. The interval between consecutive subnormal numbers is the same as the interval between consecutive normalised binary64 values that lie immediately above the underflow gap - a value known as machine epsilon. If the result of a floating-point calculation lies within the underflow gap, its value is converted to the nearest subnormal value (which could, of course, be zero).

According to the above definition, we should be able to obtain a value for machine epsilon by subtracting the smallest positive normal binary64 number (2.2250738585072014E-308) from the next smallest positive binary64 number (2.2250738585072019E-308), which results in a value of 5e-324, which you may recall is the value of Number.MIN_VALUE. The interval between any two adjacent subnormal numbers, as well as the interval between zero and Number.MIN_VALUE, is equal to machine epsilon.

Subnormal numbers are too small to be represented as normal floating-point numbers. They are still represented as 64-bit values, but in a format somewhat different to that of normal floating-point numbers. In a subnormal (or denormalised) number, all of the exponent bits are set to zero. If at least one bit in the significand is non-zero, the value is interpreted as a subnormal number with an exponent of -1022, otherwise it will be interpreted as zero. The "hidden" bit before the binary point now has a value of 0.

The value of a subnormal number Ns  is given by the following formula:

Ns   =  (-1)sign × 2-1022 × 0.f

where f is the (fractional) value stored in the significand. Here is the binary64 representation of the smallest positive non-zero subnormal value:

Sign bit: 0
Exponent: 00000000000
Significand: 0000000000000000000000000000000000000000000000000001

Inserting these values into the formula for a subnormal number, we get:

Ns   =  (-1)0 × 2-1022 × 2-52
  =  1 × 2-1074
  =  4.9406564584124654417656879286822e-324

This is not quite the same value as Number.MIN_VALUE (5e-324), and we have been unable to find any literature that explains why there is this discrepancy. It may have been a matter of notational convenience, since both of these base-10 values are represented by exactly the same binary64 subnormal number, and could be used interchangeably in calculations involving subnormal values. According to MDN's documentation:

"Number.MIN_VALUE is the smallest positive number . . . that can be represented within float precision - in other words, the number closest to 0 . . . In practice, its precise value in mainstream engines . . . is 2-1074, or 5E-324."

The Number object

JavaScript's built-in Number object provides a wrapper for primitive values of type number. If the Number() method is used with the new keyword, it acts as a constructor method and creates another object of type Number. If used without the new keyword, it creates a primitive of type number. In practice, we rarely need to create additional Number objects, because the properties and methods of the Number object are available to number primitives. Consider the following code:

const numPr1 = 100;
const numPr2 = Number(100);
const numObj = new Number(100);

console.log(typeof numPr1); // number
console.log(typeof numPr2); // number
console.log(typeof numObj); // object

console.log(numObj === numPr2); // false
console.log(numObj == numPr2); // true
console.log(numPr2 === numPr1); // true
console.log(numObj.valueOf()); // 100

The first thing to note here is that Number() used as a function creates a primitive of type number. The following statements both create primitive values of type number:

const numPr1 = 100;
const numPr2 = Number(100);

Using the new keyword with Number() creates a Number object, although if we pass an argument of 100, the value of that object will be 100. That's why the following two lines of code return different results:

console.log(numObj === numPr2); // false
console.log(numObj == numPr2); // true

The == operator (equality) and the === operator (strict equality) differ in that the == operator carries out type conversion before it makes a comparison, and subsequently only compares the values of its operands. The === operator, on the other hand, compares both the values and the data types of its operands in order to establish equality. As a rule, you should avoid creating objects of type Number.

Number object properties

The following table lists the Number object's properties and briefly describes the purpose of each.



Number object properties
PropertyDescription
Number.EPSILON

A static property that represents the difference between 1 and the smallest floating-point value of type number greater than 1 (2.220446049250313e-16).

Number.MAX_SAFE_INTEGER

A static property that returns the maximum safe integer value that can be taken by a variable of type number (253-1 or 9,007,199,254,740,991).

Number.MAX_VALUE

A static property that returns the maximum positive value that can be taken by a variable of type number (1.7976931348623157e+308).

Number.MIN_SAFE_INTEGER

A static property that returns the minimum safe integer value that can be taken by a variable of type number (-253+1 or -9,007,199,254,740,991).

Number.MIN_VALUE

A static property that returns the smallest positive value that can be taken by a variable of type number (5e-324).

Number.NaN

A static property that represents Not-A-Number - equivalent to the global NaN property.

Number.NEGATIVE_INFINITY

A static property that represents any negative value that has a magnitude greater than that of Number.MAX_VALUE.

Number.POSITIVE_INFINITY

A static property that represents any positive value that has a magnitude greater than that of Number.MAX_VALUE.



Number object methods

The Number object has a number of methods that are available to any primitive of type number. The following table lists the Number object's methods and briefly describes the purpose of each. Note that some of the methods defined for the Number object have global equivalents that have either the same, or similar, functionality.



Number object methods
MethodDescription
Number.isFinite()

A static method that returns true if the argument passed to it evaluates to a finite number, and false if it evaluates to positive infinity, negative infinity, or NaN. For example:

console.log(Number.isFinite(2e+308)); // false
console.log(Number.isFinite("Hello!")); // false
console.log(Number.isFinite(Infinity)); // false
console.log(Number.isFinite(123456)); // true

Similar to the global isFinite() method except that it does not convert the argument passed to it to a number. Only arguments of type number that are finite will return true. Non-number values always return false.

Number.isInteger()

A static method that returns true if the argument passed to it is an integer value, otherwise returns false. For example:

console.log(Number.isInteger(255)); // true
console.log(Number.isInteger("97")); // false
console.log(Number.isInteger(3.14)); // false
console.log(Number.isInteger(9 / 3)); // true
console.log(Number.isInteger(9 / 2)); // false

Number.isNan()

A static method that returns true if the argument passed to it is the number value NaN, otherwise returns false. For example:

console.log(Number.isNaN(NaN)); // true
console.log(Number.isNaN("97.5")); // false
console.log(Number.isNaN(0/0)); // true
console.log(Number.isNaN(57)); // false

Similar to the global isNaN() method except that it does not convert the argument passed to it to a number. Only arguments of type number that are also NaN will return true. Non-number values always return false.

Number.isSafeInteger()

A static method that returns true if the argument passed to it is an integer value in the range -253 +1 to 253 -1, otherwise returns false. For example:

console.log(Number.isSafeInteger(55)); // true
console.log(Number.isSafeInteger(55.7)); // false
console.log(Number.isSafeInteger(2**53)); // false
console.log(Number.isSafeInteger(2**53-1)); // true
console.log(Number.isSafeInteger(NaN)); // false
console.log(Number.isSafeInteger(Infinity)); // false

Number.parseFloat()

A static method that parses the string value supplied to it as an argument and returns a floating-point number or, if the string value cannot be coerced to a floating-point number, returns NaN. For example:

console.log(Number.parseFloat(" 123.45 ")); // 123.45
console.log(Number.parseFloat("2 hundred")); // 2.4
console.log(Number.parseFloat("Hello!")); // NaN
console.log(Number.parseFloat("1e+308")); // 1e+308
console.log(Number.parseFloat("2e+308")); // Infinity

Leading and trailing spaces are ignored.

This method has the same functionality as the global parseFloat() function.

Number.parseInt()

A static method that parses the string value supplied to it as its first argument and returns an integer value using the radix passed to it as the (optional) second argument, or to base-10 if no radix is specified or is set to 0 . For example:

console.log(Number.parseInt(" 123 ")); // 123
console.log(Number.parseInt("Temp: 98.6")); // NaN
console.log(Number.parseInt("123.45")); // 123
console.log(Number.parseInt("Hello!")); // NaN
console.log(Number.parseInt("1e+308")); // 1
console.log(Number.parseInt("1111", 2)); // 15
console.log(Number.parseInt("0xfa")); // 250

If specified, the radix argument must be either 0 or an integer value in the range 2 - 36, otherwise NaN is returned. If the radix argument is undefined or 0, base-10 is assumed.

Leading and trailing spaces are ignored. If the first non-whitespace character cannot be converted to a number, NaN is returned. The only exception is when a number is prefixed with 0x or 0X, in which case a radix of 16 is assumed.

This method has the same functionality as the global parseInt() function.

toExponential()

An instance method that returns a string representing the number on which it is called in exponential notation, with one digit before the decimal point. For example:

const myFloat = 123.4567;
const myInt = 1234567;
const MyStr = "123.4567";

console.log(myFloat.toExponential()); // 1.234567e+2
console.log(myFloat.toExponential(3)); // 1.235e+2
console.log(myInt.toExponential()); // 1.234567e+6
console.log(myInt.toExponential(4)); // 1.2346e+6
console.log(myInt.toExponential(120));
// Uncaught RangeError: precision 120 out of range
console.log(MyStr.toExponential());
// Uncaught TypeError: MyStr.toExponential is not a function

The method accepts an optional fractionDigits argument that specifies the number of digits to display after the decimal point. If used, the fractionDigits argument must be an integer value in the range 0 - 100.

If not specified, the fractionDigits argument defaults to the number of digits necessary to represent the number with maximum precision. Otherwise, if the number has more digits than requested by fractionDigits, the number is rounded to the nearest representation possible using the specified number of digits.

If the fractionDigits argument is a number outside the range 0-100, a range error is thrown.

If the method is invoked on a variable that is not a number, a type error is thrown.

toFixed()

An instance method that returns a string representing the number on which is called using fixed-point notation, whereby a fixed number of digits are used to represent the fractional part of the number. For example:

const myFloat = 123.4567;
const myInt = 1234567;
const MyStr = "123.4567";

console.log(myFloat.toFixed()); // 123
console.log(myFloat.toFixed(2)); // 123.46
console.log(myInt.toFixed()); // 1234567
console.log(myInt.toFixed(4)); // 1234567.0000
console.log((myInt * 10 ** 23).toFixed()); // 1.234567e+29
console.log(myInt.toFixed(120));
// Uncaught RangeError: precision 120 out of range
console.log(MyStr.toFixed());
// Uncaught TypeError: MyStr.toFixed is not a function

The method accepts an optional digits argument that specifies how many digits to display after the decimal point. If used, the digits argument must be an integer value in the range 0 - 100. If not specified, the digits argument defaults to 0.

If the number has more digits in its fractional part than requested by digits, the number is rounded to the nearest representation possible using the specified number of digits.

If the fractional part of a number has fewer digits than requested, the fractional part is padded with zeros in order to achieve the specified length.

If the digits argument is a number outside the range 0-100, a range error is thrown.

If the method is invoked on a variable that is not a number, a type error is thrown.

If the number on which toFixed() is called is greater than or equal to 1021, the string will be returned using exponential notation.

If the magnitude of the number on which toFixed() is called is too large to be represented in JavaScript, the method returns Infinity, -Infinity or NaN.

toLocaleString()

An instance method that returns a string representing the number on which it is called using a language-specific format. For example:

const myNum = 123456789;

console.log(myNum.toLocaleString()); // 123,456,789
console.log(myNum.toLocaleString("en-IN")); // 12,34,56,789
console.log(myNum.toLocaleString("de-DE")); // 123.456.789
console.log(myNum.toLocaleString("de-DE",
  { style: "currency", currency: "EUR" }));
// 123.456.789,00 €

The method accepts two arguments, both of which are optional. The first of these is the locales argument, which if used typically consists of a two-character language code and a two-character country code separated by a hyphen.

The second argument is the options argument, an object whose properties are used to more precisely specify the output format.

If the toLocaleString() method is called without arguments, it returns a string formatted according to the default locale, with default options.

toPrecision()

An instance method that returns a string representing the number on which is called to the specified precision. For example:

const myNum = 123.456;

console.log(myNum.toPrecision()); // 123.456
console.log(myNum.toPrecision(2)); // 1.2e+2
console.log(myNum.toPrecision(4)); // 123.5
console.log(myNum.toPrecision(6)); // 123.456
console.log(myNum.toPrecision(10)); // 123.4560000

The optional precision argument specifies the number of significant digits that should be used to represent the number on which the toPrecision() method is called.

If the precision argument is not an integer, it is rounded to the nearest integer. If no precision argument is suppplied, the method behaves in the same manner as toString() used without a radix argument (see below).

If the precision argument is a number outside the range 0-100, a range error is thrown.

toString()

An instance method that returns a string representing the number on which it is called. For example:

const myNum = 123456;

console.log(myNum.toString()); // 123456
console.log(myNum.toString(2)); // 11110001001000000
console.log(myNum.toString(8)); // 361100
console.log(myNum.toString(16)); // 1e240
console.log((myNum/1000).toString()); // 123.456

The toString() method takes an optional radix argument that specifies the number base used to present the numeric value it is called on. The values accepted are integer values in the range 2-36. When the radix argument is not specified, or is set to 0, it defaults to a value of 10.

Note that, if a radix argument of 2, 8 or 16 is passed to toString(), the string returned will be a correct binary, octal or hexadecimal representation of the number toString() is called on but will not include the binary (0b), octal (0o), or hexadecimal (0x) prefix that is normally used to signify the base of the number represented.

If the radix argument is less than 2 or greater than 36, a range error is thrown.

If the magnitude of the number on which toString() is called is greater than or equal to 1021 or less than 10-6, the string will be returned using exponential notation.

valueOf()

An instance method that returns the primitive value of the number it is called on. For example:

const myNumObj = new Number(0xffff);
console.log(myNumObj.valueOf()); // 65535
console.log(typeof myNumObj); // object

const myNum = myNumObj.valueOf();
console.log(myNum); // 65535
console.log(typeof myNum); // number

This method does not take any arguments. It is usually called internally by JavaScript and rarely used explicitly in a web application.

The Math object

JavaScript's built-in Math object provides a number of useful mathematical constants and functions. It facilitates the creation of code to handle complex mathematical operations. Unlike most other JavaScript objects, the Math object has no constructor method. You do not need to create an instance of the Math object in order to access its properties and use its methods.

The Math object's static properties include widely-used mathematical constants such as e (Euler's number), which is used for calculations involving exponential growth and decay, both in scientific research and in the finance sector. Other static properties include constants that are ubiquitous in many areas of science, engineering and mathematics, such as the square roots of ½ and 2, frequently used logarithmic values, and of course pi (π), which features heavily in countless geometric and trigonometric formulae used in many branches of mathematics, science and engineering.

The Math object also provides methods that can be used to help us carry out trigonometric calculations, find the sign and absolute value of a number, calculate roots and logarithmic values, find the maximum or minimum value in a set of numeric variables, work with exponential values, truncate or round floating-point numbers, or generate pseudo-random numbers.

The last-mentioned feature - the ability to generate random numbers - will be of particular interest to those engaged in developing gaming applications, because these applications often rely on being able to create random events. An algorithm that produces random numbers is called a random number generator (RNG). Let's see how we might code a web page that can generate six randomly selected lottery numbers in the range 1-49. Here is the code:

<!doctype html>
<html lang="en">

  <head>
    <meta charset="utf-8">
    <title>JavaScript Demo 65</title>
    <style>
      h1 {text-align: center}
      table {
        padding: 0.5em;
        margin: auto;
      }
      th, td {
        border: 1px solid grey;
        padding: 0.5em;
        text-align: center;
      }
      div {
        text-align: center;
        padding: 0.5em;
      }
    </style>
  </head>

  <body>
    <h1>Lottery Number Generator</h1>

    <table>
      <caption>Lottery Numbers</caption>
      <thead>
        <tr>
          <th>1st</th><th>2nd</th><th>3rd</th><th>4th</th><th>5th</th><th>6th</th>
        </tr>
      </thead>
      <tbody>
        <tr id="numArray">
          <td>-</td><td>-</td><td>-</td><td>-</td><td>-</td><td>-</td>
        </tr>
      </tbody>
    </table>
    <div>
      <button id="lottoGo">Generate Numbers</button>
    </div>

    <script>
      let btn = document.querySelector("#lottoGo");

      btn.addEventListener("click", generateLotteryNumbers);

      function getRandomIntInRange(min, max) {
        return Math.floor(Math.random() * (max - min + 1) + min);
      }

      function numCompare(a, b) { return a - b; }

      function generateLotteryNumbers() {
        const lottery = new Array(6);
        const elements = document.getElementById("numArray").children;
        let i = 0;
        while(i < 6) {
          let num = getRandomIntInRange(1,49);
          if(!lottery.includes(num)) {
            lottery[i] = num;
            i++;
          }
        }
        lottery.sort(numCompare);
        for(let i=0; i<6; i++) {
          elements[i].innerHTML = lottery[i];
        }
      }
    </script>

  </body>
</html>

Copy and paste this code into a new file in your HTML editor, save the file as javascript-demo-66.html, open the file in a web browser, and click on the "Generate Numbers" button. You should see something like the illustration below, depending on what random numbers have been generated.


A simple random number generator

A simple random number generator


If you study the JavaScript code, you will see that we have used two methods belonging to the Math object - Math.random() and Math.floor(). The random numbers are created by the getRandomIntInRange() function. This function finds the difference between the values it receives for min and max, adds 1 to that number, multiplies the result by the output of Math.random() to create a pseudo-random floating-point number, and then adds the value of min to the result. The Math.floor() method is then used to round the result down to the nearest integer value.

The remaining code is fairly self-explanatory. The generateLotteryNumbers() function creates an empty 6-element array to hold the lottery numbers, and calls getRandomIntInRange() repeatedly using a while loop until the array contains 6 unique integer values in the range 1-49. We avoid duplicating any of the numbers in the array by checking each randomly generated number to see if it is already included, in which case it is not added to the array.

Once we have an array containing six different numbers between 1 and 49, the values in the array are sorted by ascending value. Each value is then inserted into a separate table data (<td> . . . </td>) element in a HTML table by assigning it to the element's innerHTML property.

Math object properties

The Math object has a number of static properties that hold mathematical constants. The following table lists the Math object's properties and briefly describes the purpose of each.



Number object properties
PropertyDescription
Math.E

A static property that represents Euler's number (e), an irrational number that is widely used in problems relating to exponential growth or decay. It is also the base of the natural logarithm. Its value to 15 decimal places is 2.718281828459045.

Math.LN10

A static property that represents the natural logarithm of 10 - the power to which Euler's number e must be raised in order to obtain a value of 10. It is an irrational number whose value to 15 decimal places is 2.302585092994046

Math.LN2

A static property that represents the natural logarithm of 2 - the power to which Euler's number e must be raised in order to obtain a value of 2. It is an irrational number whose value to 16 decimal places is 0.6931471805599453.

Math.LOG10E

A static property that represents the base-10 logarithm of Euler's number (e) - the power to which 10 must be raised to obtain a value of e. It is an irrational number whose value to 16 decimal places is 0.4342944819032518.

Math.LOG2E

A static property that represents the base-2 logarithm of Euler's number (e) - the power to which 2 must be raised to obtain a value of e. It is an irrational number whose value to 16 decimal places is 1.4426950408889634.

Math.PI

A static property that represents the mathematical constant π (the Greek letter pi), which is the ratio of the circumference of any circle to the diameter of the circle. It is an irrational number whose value to 15 decimal places is 3.141592653589793.

Math.SQRT1_2

A static property that represents the square root of ½. It is an irrational number whose value to 16 decimal places is 0.7071067811865476.

Math.SQRT2

A static property that represents the square root of 2. It is an irrational number whose value to 16 decimal places is 1.4142135623730951.



Math object methods

The JavaScript Math object has a number of methods to facilitate the performance of mathematical operations. The following table lists the Math object's methods and briefly describes the purpose of each.



Math object methods
MethodDescription
Math.abs()

A static method that returns the absolute value of the number passed to it as an argument, regardless of sign. For example:

console.log(Math.abs(1)); // 1
console.log(Math.abs(-1)); // 11
console.log(Math.abs(Infinity)); // Infinity1
console.log(Math.abs(-Infinity)); // Infinity1
console.log(Math.abs(255 - 270)); // 151
console.log(Math.abs(null)); // 01
console.log(Math.abs(undefined)); // 01
console.log(Math.abs("Hello!")); // NaN1

The Math.abs() method attempts to coerce the argument it receives to a value of type number. If a value cannot be coerced to a number, it returns NaN.

Math.acos()

A static method that returns the inverse cosine, in radians, of the number passed to it as an argument. For example:

console.log(Math.acos(2)); // NaN
console.log(Math.acos(1)); // 0
console.log(Math.acos(0.5)); // 1.0471975511965979
console.log(Math.acos(0)); // 1.5707963267948966
console.log(Math.acos(-0.5)); // 2.0943951023931957
console.log(Math.acos(-1)); // 3.141592653589793
console.log(Math.acos(-2)); // NaN

The argument supplied to Math.acos() represents the cosine of an angle, and must be a number in the range -1 to 1. The value returned represents the inverse cosine - an angle in radians, with a value in the range 0 to π. If the argument supplied has a value of less than -1 or greater than 1, Math.acos() returns NaN.

Math.acosh()

A static method that returns the inverse hyperbolic cosine of the number passed to it as an argument. For example:

console.log(Math.acosh(0)); // NaN
console.log(Math.acosh(1)); // 0
console.log(Math.acosh(2)); // 1.3169578969248166
console.log(Math.acosh(100)); // 5.298292365610484

The argument supplied to Math.acosh() must have a value of 1 or greater. If the argument is less than 1, Math.acosh() returns NaN.

Math.asin()

A static method that returns the inverse sine, in radians, of the number passed to it as an argument. For example:

console.log(Math.asin(-2)); // NaN
console.log(Math.asin(-1)); // -1.5707963267948966
console.log(Math.asin(-0.5)); // -0.5235987755982989
console.log(Math.asin(0)); // 0
console.log(Math.asin(0.5)); // 0.5235987755982989
console.log(Math.asin(1)); // 1.5707963267948966
console.log(Math.asin(2)); // NaN

The argument supplied to Math.asin() represents the sine of an angle, and must be a number in the range -1 to 1. The value represents the inverse sine - an angle in radians with a value in the range -π/2 to π/2. If the argument supplied has a value of less than -1 or greater than 1, Math.asin() returns NaN.

Math.asinh()

A static method that returns the inverse hyperbolic sine of the number passed to it as an argument. For example:

console.log(Math.asinh(-100)); // -5.298342365610589
console.log(Math.asinh(-1)); // -0.881373587019543
console.log(Math.asinh(0)); // 0
console.log(Math.asinh(1)); // 0.881373587019543
console.log(Math.asinh(100)); // 5.298342365610589

Math.atan()

A static method that returns the inverse tangent, in radians, of the number passed to it as an argument. For example:

console.log(Math.atan(-100)); // -1.5607966601082315
console.log(Math.atan(-1)); // -0.7853981633974483
console.log(Math.atan(0)); // 0
console.log(Math.atan(1)); // 0.7853981633974483
console.log(Math.atan(100)); // 1.5607966601082315

Math.atan2()

A static method that returns the angle, in radians, between the positive x-axis and the ray from the origin (0, 0) to the point x, y whereby the coordinate values x and y are the first and second arguments passed to Math.atan2(). For example:

console.log(Math.atan2(3, 4)); // 0.6435011087932844
console.log(Math.atan2(-3, -4)); // -2.498091544796509
console.log(Math.atan2(3, -4)); // 2.498091544796509
console.log(Math.atan2(-3, 4)); // -0.6435011087932844

Math.atanh()

A static method that returns the inverse hyperbolic tangent of the number passed to it as an argument. For example:

console.log(Math.atanh(-2)); // NaN
console.log(Math.atanh(-1)); // -Infinity
console.log(Math.atanh(-0.5)); // -0.5493061443340548
console.log(Math.atanh(0)); // 0
console.log(Math.atanh(0.5)); // 0.5493061443340548
console.log(Math.atanh(1.0)); // Infinity
console.log(Math.atanh(2)); // NaN

The argument supplied to Math.atanh() must be a number in the range -1 to 1. If the argument suplied is 1, Math.atanh() returns Infinity. If the argument is -1, it returns -Infinity, and If it has a value of less than -1 or greater than 1, it returns NaN.

Math.cbrt()

A static method that returns the cube root of the number passed to it as an argument. For example:

console.log(Math.cbrt(-1)); // -1
console.log(Math.cbrt(0)); // 0
console.log(Math.cbrt(2)); // 1.2599210498948732
console.log(Math.cbrt(27)); // 3
console.log(Math.cbrt(-125)); // -5

Math.ceil()

A static method that rounds up the number passed to it as an argument to the nearest integer, returning the smallest integer greater than or equal to that number. For example:

console.log(Math.ceil(-1)); // -1
console.log(Math.ceil(2.4)); // 3
console.log(Math.ceil(12.6)); // 13
console.log(Math.ceil(21)); // 21
console.log(Math.ceil(-12.5)); // -12

Math.clz32()

A static method that returns the number of leading zero bits in the 32-bit binary representation of the number passed to it as an argument (clz32 stands for Count Leading Zeros 32). For example:

console.log(Math.clz32(-1)); // 0
console.log(Math.clz32(0)); // 32
console.log(Math.clz32(12.5)); // 28
console.log(Math.clz32(255)); // 24
console.log(Math.clz32(2 ** 53)); // 0
console.log(Math.clz32("Hello!")); // 32

If the argument passed to Math.clz32() is not a number, it will first be coerced to a number, and then converted to an unsigned 32-bit integer. If the argument is 0 or has a value greater than or equal to 231, the value returned will be 0. If the argument cannot be coerced to a number, the value returned will be 32.

Math.cos()

A static method that returns the cosine, in radians, of the number passed to it as an argument. For example:

console.log(Math.cos(0)); // 1
console.log(Math.cos(1)); // 0.5403023058681398
console.log(Math.cos("1")) // 0.5403023058681398
console.log(Math.cos(Math.PI)); // -1
console.log(Math.cos(Math.PI * 2)); // 1

Math.cosh()

A static method that returns the hyperbolic cosine of the number passed to it as an argument. For example:

console.log(Math.cosh(-1)); // 1.5430806348152437
console.log(Math.cosh(0)); // 1
console.log(Math.cosh(1)); // 1.5430806348152437
console.log(Math.cosh("1")); // 1.5430806348152437

Math.exp()

A static method that returns e raised to the power of the number passed to it as an argument, where e is the base of the natural logarithm. For example:

console.log(Math.exp(-1)); // 0.36787944117144233
console.log(Math.exp(0)); // 1
console.log(Math.exp(1)); // 2.718281828459045

Note that if the argument supplied is a value very close to 0, the value returned will be very close to 1 and will suffer from a loss of precision. In such cases, it might be better to use the Math.expm1() method, for which the fractional part of the return value has a much higher precision.

Math.expm1()

A static method that raises e to the power of the number passed to it as an argument, where e is the base of the natural logarithm, and returns the result minus 1. For example:

console.log(Math.expm1(-1)); // -0.6321205588285577
console.log(Math.expm1(0)); // 0
console.log(Math.expm1(1)); // 1.718281828459045

Math.floor()

A static method that rounds down the number passed to it as an argument to the nearest integer, returning the largest integer less than or equal to that number. For example:

console.log(Math.ceil(-1)); // -1
console.log(Math.ceil(2.4)); // 2
console.log(Math.ceil(12.6)); // 12
console.log(Math.ceil(21)); // 21
console.log(Math.ceil(-12.5)); // -13

Math.fround()

A static method that returns the nearest 32-bit single-precision representation of the number passed to it as an argument. For example:

const val64 = 1.234;
const val32 = Math.fround(1.234);

console.log(val64); // 1.234
console.log(val32); // 1.2339999675750732
console.log(val64 === val32); // false
console.log(Math.fround(1.5) === 1.5); // true
console.log(Math.fround(2 ** 128)); // Infinity

The Math.fround() method is useful if you are working with 32-bit floating-point numbers and need to test for equality with JavaScript variables of type number.

The number to which Math.fround() is applied is still stored as a 64-bit double-precision floating-point value internally, but is rounded to even on the 23rd bit of the significand. All bits beyond that are set to 0.

When Math.fround() is applied to a 64-bit floating point number like 1.234, which does not have an exact binary representation, the value returned will not match the standard 32-bit representation of that number.

Numbers like 1.5 that have an exact binary representation as both 32-bit and 64-bit floating-point numbers will be regarded as the same value when tested for equality.

If the argument supplied is a number too big to be represented by a 32-bit floating-point number, Infinity or -Infinity is returned.

Math.hypot()

A static method that returns the square root of the sum of the the squares of the numbers passed to it as arguments. For example:

console.log(Math.hypot()); // 0
console.log(Math.hypot(3, 4)); // 5
console.log(Math.hypot(-3, 4)); // 5
console.log(Math.hypot(6, 8)); // 10
console.log(Math.hypot("17", 29, 31)); // 45.727453460694704

Returns 0 if no arguments are supplied or all of the arguments supplied are ±0. Returns Infinity if any of the arguments supplies are ±Infinity. Returns NaN if one or more of the arguments supplied is not a number and cannot be coerced to a number.

Math.imul()

A static method that returns the product of the two integer values passed to it as arguments. For example:

console.log(Math.imul()); // 0
console.log(Math.imul(0, 3)); // 0
console.log(Math.imul(2, 8)); // 16
console.log(Math.imul(-3, 4)); // -12
console.log(Math.imul(6, 8)); // 48
console.log(Math.imul(4, 5, 6)); // 20

Returns 0 if no arguments are supplied, or if one of the arguments supplied is 0. If more than two arguments are supplied, all arguments other than the first two are ignored. If either of the arguments supplied have a fractional part, the fractional part is ignored.

Math.log()

A static method that returns the natural logarithm (log to base e) of the number passed to it as an argument. For example:

console.log(Math.log(-1)); // NaN
console.log(Math.log()); // NaN
console.log(Math.log(0)); // -Infinity
console.log(Math.log(2.4)); // 0.8754687373538999
console.log(Math.log(18)); // 2.8903717578961645

Returns NaN if no argument is supplied or the argument supplied is less than 0, -Infinity if any the argument is ±0.

Note that if the argument supplied is a value very close to 1, the value returned can suffer from a loss of precision. In such cases, it might be better to use the Math.log1p() method.

Math.log10()

A static method that returns the logarithm to base 10 ) of the number passed to it as an argument. For example:

console.log(Math.log10(-1)); // NaN
console.log(Math.log10());// NaN
console.log(Math.log10(-0)); // -Infinity
console.log(Math.log10(2.4)); // 0.38021124171160603
console.log(Math.log10(18)); // 1.255272505103306

Returns NaN if no argument is supplied or the argument suppliedis less than 0, -Infinity if any the argument is ±0.

Math.log1p()

A static method that returns the natural logarithm (log to base e) of the number passed to it as an argument + 1. For example:

console.log(Math.log1p(-2)); // NaN
console.log(Math.log1p(-1)); // -Infinity
console.log(Math.log1p(0)); // 0
console.log(Math.log1p(1)); // 0.6931471805599453
console.log(Math.log1p(10)); // 2.3978952727983707

Returns NaN if no argument is supplied or the argument supplied is less than -1, and -Infinity if the argument supplied is -1.

Math.log2()

A static method that returns the logarithm to base 2 of the number passed to it as an argument + 1. For example:

console.log(Math.log2()); // NaN
console.log(Math.log2(-1)); // NaN
console.log(Math.log2(0)); // -Infinity
console.log(Math.log2(1)); // 0
console.log(Math.log2(10)); // 3.321928094887362

Returns NaN if no argument is supplied or the argument supplied is less than 0, and -Infinity if the argument supplied is 0.

Math.max()

A static method that returns the largest of the numbers passed to it as arguments. For example:

console.log(Math.max()); // -Infinity
console.log(Math.max(-10, -12, -35)); // -10
console.log(Math.max(3, 5, 7, 11)); // 11
console.log(Math.max(10, 35, "Hello!")); // NaN

Returns -Infinity if no arguments are supplied, and NaN if any of the arguments suppied is, or evaluates to, NaN.

Math.min()

A static method that returns the smallest of the numbers passed to it as arguments. For example:

console.log(Math.min()); // Infinity
console.log(Math.min (-10, -12, -35)); // -35
console.log(Math.min(3, 5, 7, 11)); // 3
console.log(Math.min(10, 35, "Hello!")); // NaN

Returns Infinity if no arguments are supplied, and NaN if any of the arguments suppied is, or evaluates to, NaN.

Math.pow()

A static method that returns the value of a base raised to a power. This method expects two numbers as arguments. The first argument is the base, and the second argument is the exponent. For example:

console.log(Math.pow()); // NaN
console.log(Math.pow(5, 2)); // 25
console.log(Math.pow(10, 3)); // 1000
console.log(Math.pow(9, 0.5)); // 3
console.log(Math.pow(27, 1/3)); // 3
console.log(Math.pow(-5, 2)); // 25
console.log(Math.pow(-5, 3)); // -125
console.log(Math.pow("Hello!", 0)); // 1
console.log(Math.pow(2, "Hello!")); // NaN
console.log(Math.pow("Hello!", 2)); // NaN
console.log(Math.pow(-1, Infinity)); // NaN
console.log(Math.pow(-2, 1/3)); // NaN

The return value will be NaN if:

  • No arguments are supplied
  • The exponent is NaN
  • The base is NaN and the exponent is not 0
  • The base is ±1 and the exponent is ±Infinity
  • The base is less than 0 and the exponent is not an integer
Math.random()

A static method that takes no arguments and returns a pseudo-random floating-point value that is greater than or equal to 0 and less than 1. This value can then be used with a given range of values to create a randon value within that range. For example:

Math.random()); // a number between 0 (inclusive) and 1 (exclusive)
Math.random() * 10); // a number between 0 (inclusive) and 10 (exclusive)
Math.random() * 250); // a number between 0 (inclusive) and 250 (exclusive)

To generate a random number that falls between two values, we could do something like this:

function getRandomInRange(min, max) {
  return Math.random() * (max - min) + min;
}

console.log(getRandomInRange(25,75));
// a number between 25 (inclusive) and 75 (exclusive)

If we want to obtain a random integer that falls between two values, we could do something like this:

function getRandomIntInRange(min, max) {
  const iMin = Math.ceil(min);
  const iMax = Math.floor(max);
  return Math.floor(Math.random() * (iMax - iMin + 1) + iMin);
}

The function getRandomIntInRange() returns an integer value in the specified range, inclusive of both the minimum and maximum integer values passed to it as arguments.

Math.round()

A static method that returns the value of the number passed to it as an argument, rounded to the neares integer. For example:

console.log(Math.round(-10.6)); // -11
console.log(Math.round(-10.5)); // -10
console.log(Math.round(10.49)); // 10
console.log(Math.round(10.5)); // 11
console.log(Math.round(10)); // 10

A positive number with a fractional part greater than or equal to 0.5 is rounded up to the next largest integer. If the fractional part is less than 0.5, the number is rounded down to the next smallest integer.

A negative number with a fractional part less than or equal to 0.5 is rounded up to the next largest integer (i.e. in the positive direction). If the fractional part is greater than 0.5, the number is rounded down to the next smallest integer (i.e. in the negative direction).

Math.sign()

A static method that returns 1 or -1, depending on whether the number passed to it as an argument is a positive or negative value, respectively. For example:

console.log(Math.sign()); // NaN
console.log(Math.sign(0)); // 0
console.log(Math.sign(-0)); // -0
console.log(Math.sign(5)); // 1
console.log(Math.sign(-5)); // -1
console.log(Math.sign("Hello!")); // NaN

If no argument is supplied, or if the argument supplied cannot be coerced to a number, Math.sign() returns NaN. If the argument supplied is 0 or -0, the return value will be the argument itself (0 or -0).

Math.sin()

A static method that returns the sine, in radians, of the number passed to it as an argument. For example:

console.log(Math.sin(0)); // 0
console.log(Math.sin(1)); // 0.8414709848078965
console.log(Math.sin(Math.PI)); // 1.2246467991473532e-16
console.log(Math.sin(Math.PI / 2)); // 1

Math.sinh()

A static method that returns the hyperbolic sine of the number passed to it as an argument. For example:

console.log(Math.sinh(-1)); // -1.1752011936438014
console.log(Math.sinh(0)); // 0
console.log(Math.sinh(1)); // 1.1752011936438014

Math.sqrt()

A static method that returns the square root of the number passed to it as an argument. For example:

console.log(Math.sqrt(-1)); // NaN
console.log(Math.sqrt(0)); // 0
console.log(Math.sqrt(1)); // 1
console.log(Math.sqrt(2)); // 1.4142135623730951
console.log(Math.sqrt(25)); // 5

Math.tan()

A static method that returns the tangent, in radians, of the number passed to it as an argument. For example:

console.log(Math.tan(0)); // 0
console.log(Math.tan(1)); // 1.5574077246549023
console.log(Math.tan(Math.PI / 2)); // 16331239353195370
console.log(Math.tan(Math.PI / 4)); // 0.9999999999999999

Note that, because of issues with floating-point precision, it is not possibe to obtain an exact value for π/2 (90°) or π/4 (45°). In trigonometry, the value of tan 90 is generally considered to be infinity (or undefined), and the value of tan 45° is 1.

Math.tanh()

A static method that returns the hyperbolic tangent of the number passed to it as an argument. For example:

console.log(Math.tanh(-1)); // -0.7615941559557649
console.log(Math.tanh(0)); // 0
console.log(Math.tanh(1)); // 0.7615941559557649

Math.trunc()

A static method that returns the integer part of a number passed to it as an argument. For example:

console.log(Math.trunc()); // NaN
console.log(Math.trunc(-1.5)); // -1
console.log(Math.trunc(0)); // 0
console.log(Math.trunc(6.4)); // 6
console.log(Math.trunc(15.75)); // 15

Any decimal digits to the right of the decimal point are simply discarded, regardless of whether the argument is a positive or negative number.