Javascript is not a language for the unwary - Kobby Owen
Just before you stop reading, I know what you are thinking! "Who would read an article about these simple functions ?. These are basic functions every beginner of Javascript can quickly master, and easily learn to use. There is no need for an article about this!". While this is true, their behavior may be somewhat surprising, especially when dealing with non-number arguments. Learning more about their implementation will give you an in-depth knowledge of JavaScript and its core implementation.
If you can answer the following questions, then you may stop reading this article. If not, I suggest you keep reading, since you will learn a lot from studying these functions and their implementation.
- Why is
isNaN(new Date())
false andisNaN(Date())
true - Why is
isFinite(null)
true andisFinite(undefined)
false - Why is
isFinite(" ")
true andisFinite("a")
false - Why is
isNaN("Infinity")
true andisNaN("infinity")
false - Under what conditions do
isNaN
andisFinite
throw a TypeError - What is the value of
isNaN(" 23 ")
,isNaN(23)
,isNaN(" 23." )
,isNaN("12 .")
isFinite
function determines if the passed argument is finite value. It checks if its argument is not NaN
, or negative infinity or positive positive infinity.
isNaN
on the other hand determines whether the argument passed is a NaN
or not. This function is necessary because of the nature of NaN
. NaN
is the only floating point value that does not compare to itself. This behavior is so true that, ECMAScript documentation suggests that one of the reliable ways to check for NaN
is the expression(x === x)
, which returns false only if x
is a NaN
.
Mostly, to determine if a number is okay to be used in arithmetic operation without few surprises, you should find yourself using isFinite
more than isNaN
, since isFinite
checks for NaN
values and goes on to check for infinite values. In cases where infinite values are legitimately allowed to participate in operations, isNaN
will be the best function to use.
Implementation Details
The first thing isNaN
and isFinite
function does is to try to convert its argument to a Number. This conversion is done using an ECMAScript internal function which is not exposed to the developer. It is this internal function that forms the core of these two functions, and is therefore worth studying. For the purpose of the article, lets call this function ToNumber
function. This function is used a lot in ECMAScript behind the scenes. Understanding how it works, will give you a lot of understanding about the results of most operations in JavaScript. In an attempt to explain this internal function, we will use many helper functions and explain other internal methods used in ECMAScript that helps the ToNumber
function to perform its conversion. I will use to top down approach to explain the implementation of this function.
ToNumber Internal Function
ToNumber
function takes a single argument, which is the argument to convert. To convert the argument, it takes the following step.
- If the argument is undefined, it returns
NaN
- If the argument is null , it returns
0
- If the argument is a number, it returns it
- If the argument is a BigInt , throw a
TypeError
- If the argument is a Symbol, throw a
TypeError
- If the argument is a String, call another internal method(
StringToNumber
) - If the argument is an Object, call another internal method(
ToPrimitive
) and pass its result throughToNumber
function again.
NB. Step 7 involves 2 steps, it calls a helper function to convert the object to a primitive value, preferably a number, and call the ToNumber
function recursively on its return value. The astute reader may reason at this point that this can cause an infinite recursion. That is not the case, because ECMAScript makes sure the return of ToPrimitive
is not another object.
Now lets look at the two helper functions used by ToNumber
to aid the conversion of its argument.
StringToNumber Internal Function
StringToNumber
function simply parses its string argument and converts it to a number. One important thing to note about this function is the kind of input the parser accepts. The parser allows for optional white-space before and after the main string decimal characters. Any invalid character present in the argument, no matter where it is, will cause the parser to return NaN
, and consequently the function too. Invalid characters include any character that is not part of the set [+ - E e .]. These valid non decimal characters are however allowed to appear only once. Making it appear twice will cause the function to return NaN
. The function however recognizes the "Infinity" and returns the mathematical representation of it. An optional + or - is allowed before the decimal characters. They should however be the first non white-space character, if it exists in the sequence except it is being used before an E or e. An empty string, or a string full of white-space will cause the function to return the number 0. The following examples demonstrates the use of the function.
function StringToNumber( argument ){
/** implementation code **/
}
StringToNumber(" 23") // 23
StringToNumber(" 23 ") // 23
StringToNumber("+23.5") // 23.5
StringToNumber("+ 23.5") // NaN ( space after the plus sign)
StringToNumber("-23.5") // -23.5
StringToNumber("23.2.3") // NaN
StringToNumber("23ab") //NaN
StringToNumber("Infinity") // Infinity
StringToNumber("-Infinity") // -Infinity
StringToNumber("+Infinity") // Infinity
StringToNumber("ab") //NaN
StringToNumber("NaN")
/**NaN ( not because the phrase NaN can be parsed , but because the characters N a N cannot be represented as a number) **/
StringToNumber("23E-14") //23E-14
StringToNumber("23E -14") //NaN ( space after E. )
ToPrimitive Internal Function
The last function to examine before we can proceed is ToPrimitive
method. This method takes an input and converts it to a primitive type, basically a number or a string. The function also takes an optional argument called hint. The hint
argument can either be [default, number or string]. When the function is called, it first checks if the input is an object. If it is and it defines a Symbol.toPrimitive
method, it is called on the object while passing "number" as a hint to the function. If the method returns an object ( null not included ), a TypeError
is thrown, otherwise its value is returned. If the object does not define its Symbol.ToPrimitive
, it looks for two methods on the object, ie toString
and valueOf
. If the hint is a number, valueOf
is called first, else toString
is called first, and the other is called next. When the function to be called first is resolved, it is checked if it exists on the object or any of its bases, if it exists, and its return value when called is not an object, it returns it results. The second function, which is based on the value passed to the hint argument is called next. Its value is returned if is is not an object. If both methods returns an object, a TypeError
is thrown by the function.
If you did not understand these functions, here are their implementation in JavaScript( note JavaScript). In a real ECMAScript implementation, these functions are probably implemented in C/C++.
function StringToNumber( argument ){
const res = argument.trim()
// return 0 for empty string after stripping space characters
if ( res.length === 0 ) return 0
return Number(res)
}
function OrdinaryToPrimitive( input, hint){
let methodNames = []
if ( hint === "string" )
methodNames = ["toString", "toValueOf"]
else
methodNames = ["valueOf", "toString"]
for ( const name of methodNames) {
if ( typeof name === "function" ){
const res = input[name]()
if ( typeof res !== 'object' || res === null)
return res
}
}
throw TypeError
}
function ToPrimitive( input, hint){
if ( typeof input === "object" ){
if ( input[Symbol.toPrimitive] !== undefined ){
if ( hint === undefined ) hint = 'default'
const res = input[Symbol.toPrimitive]( hint )
if ( typeof res !== 'object' || res === null)
return res
throw TypeError
}
else{
if ( hint === undefined ) hint = "number"
return OrdinaryToPrimitive(input, hint)
}
}
return input
}
function ToNumber( argument ) {
switch( typeof argument) {
case 'undefined' :
return NaN
case 'number' :
return argument
case 'bigint': case 'symbol':
throw TypeError
case 'string' :
return StringToNumber(argument)
case 'object':{
if (argument === null )
return 0
const hint = "number"
const primitive = ToPrimitive(argument, hint)
return ToNumber(primitive)
}
}
}
There are a few things to note here. ToPrimitive
delegates to another method called OrdinaryToPrimitive
if the input provided does not defined Symbol.toPrimitive
method.
isNaN and isFinite
Now that we understand these internal functions. Let us go back to our functions.
isNaN
first converts its argument to a number using the ToNumber
method and checks for NaN
. If the result of that conversion is a NaN
, true is return otherwise false is returned.
isFinite
also first converts its argument to a number using the same ToNumber
method. If then proceeds to check if the result of that conversion is not a NaN
or -Infinity
or Infinity
.
There is nothing interesting about these functions apart from the internal method that it calls to convert its argument before checking it. ToNumber
internal methods are used by a lot of JavaScript functions including parseInt
to convert its radix
argument., All functions defined on the global Math object calls the function on its arguments before it starts processing the result, it is used by Date.UTC
to convert it parameters into acceptable values and almost all the setter methods on the Date object ( example setHours
, setMonth
, setYear
) and almost all methods and functions that operates with numbers. Understanding how this internal method works will save you from opening your jaws wide while you stare at the screen trying to understand the return values of some functions. Try to take a moment to go through this internal method one more time. Now let us answer the five questions at the beginning of the article, which you should be able to answer if you paid enough attention to it.
Question 1
Why is isNaN(new Date())
false and isNaN(Date())
true
Answer
The result of new Date()
is an object. When that object is passed to isNaN
as an argument, ToPrimitive
is called to convert it to a primitive value, preferably a number. This ends up calling valueOf
method on the object and returning its results, which is a number. This number is then checked for NaN
, which is ultimately false. The result of Date()
on the other hand is a string that represents the current time. This string is passed to StringToNumber
internal method by ToNumber
. The result is a string that cannot be parsed into a number, thus returning NaN
. isNaN
proceeds to check the result of this conversion and finds that its NaN
and ultimately return true
Question 2
Why is isFinite(null)
true and isFinite(undefined)
false
Answer
ToNumber
converts null to 0 and undefined to NaN
, thus the return values of isFinite
when called with these two values
Question 3
Why is isFinite(" ")
true and isFinite("a")
false
Answer
Both arguments are strings, so ToNumber
calls StringToNumber
internal method on them. Empty strings after trimming white spaces causes the method to return 0. Thus the first isFinite
call is the result of checking if 0 is a finite number, which it is. "a" on the other hand returns NaN
when converted.
Question 4
Why is isNaN("Infinity")
true and isNaN("infinity")
false
Answer
StringToNumber
recognizes the string "Infinity" , "-Infinity", "-Infinity". It rightly returns Infinity and the result is checked whether its NaN
, which ends up being false. Infinity
is not NaN
.
"infinity" on the other hand is not recognized, neither can it be parsed as a number. It returns NaN
as a result of the conversion.
Question 5.
Under what conditions do isNaN
and isFinite
throw a TypeError
Answer
If their argument in either a BigInt
, Symbol or they defined toString
and valueOf
which both returns an object instead of a primitive value like a string or a number
Question 6.
What is the value of isNaN(" 23 ")
, isNaN(23)
, isNaN(" 23." )
, isNaN("12 .")
Answer
isNaN(" 23 ")
is false
isNaN("23.")
is false
isNaN("12 .")
is true
Top comments (1)
Great article, but I have two concerns:
isNaN("Infinity")
true andisNaN("infinity") false
" shouldn't it be the opposite? ActuallyisNaN("Infinity")
does returnfalse
andisNaN("infinity")
does returntrue
.StringToNumber
recognizes de string "Infinity", "-Infinity", "-Infinity"". What did you really mean here?