Difference between "==" & "==="

Difference between "==" & "==="

"A little knowledge is a dangerous thing".

In this blog, I will talk about a specific topic about which I ask everyone and I got the same answer(which is not correct obviously). The topic which I will be covering in this blog is the difference between "==" and "===".

As we know "==" is Loose equality and "===" is Strict equality". The misconception that I earlier had:

  1. "==" equality checks for value

  2. "===" equality checks for both value and datatype

     const numAge = 20;
     console.log(typeof numAge); /* "number" */
     const strAge = "20";
     console.log(typeof strAge); /* "string" */
     console.log(numAge == strAge);   /* true */
     console.log(numAge === strAge);  /* false */
    

If you ask anyone about the difference between these two equality operators, most of them will answer the above two points without any hesitation(not knowing they are wrong). The code above also appears to be in sync with the two points mentioned above. But in reality, our mental modal regarding the working of the equality operators is completely wrong. To correct this mental modal, read the complete blog properly and be patient.

I know it's not easy for a beginner to understand the complexity of two, but we should make efforts to understand as:

"When there is a difference between what we think and what JS Does, there appears the bug".

Before going in-depth explanation of operators I would like to throw some light on Type coercion and Abstract operations.

Type Coercion:

Type coercion is related to mutating the data type of values.

Type Coercion is of two types:

  1. Implicit Coercion:

    When there is a change in the data type automatically by the language itself. Javascript implicitly performs automatic type conversion as needed.

     /* Implicit Coercion */
     const numOne = "2";
     const numTwo = 5;
     console.log(numOne + numTwo); /* "2" + 5  = "2" + "5" = "25"
    
     /* In the above code we are doing string concatenation. If one of the 
     operand is a string then other operand is automatically converted to 
     the string data type. In the above code, numTwo data type is a number 
     which is converted to string automatically by implicit coercion. */
    
  2. Explicit Coercion:

    When there is a change in the data type intentionally(on purpose) by the author of the code, then it is said to be explicit coercion.

     /* Explicit Coercion */
     const valueOne = "32";
     console.log(typeof valueOne);   /* "string" */
     console.log(Number(valueOne));  /* 32 */
     console.log(typeof Number(valueOne));  /* "number" */
     /* In the above code, it seems that the author of the code had 
     intentionally done coercion (data type conversion) "32" (string) to 
     32 (number). */
    

Abstract Operations:

According to the Javascript specification, abstract operations are not part of the ECMA script language, but they are solely responsible for type conversion in Javascript. This simply means that they happen only internally, they are not like functions or methods that we can call. So whenever coercion occurs whether it is implicit or explicit, one or more internal operations knowns as abstract operations are formed. When we call them abstract we mean they are conceptual operations.

Here are some Abstract Operations(AO):

  1. ToPrimitive AO

  2. ToNumber AO

ToPrimitive Abstract Operation:

When a non-primitive value is used in a context that requires a primitive value, the Javascript engine invokes the ToPrimitive abstract operation. It takes a non-primitive value and converts it to a primitive value.

According to the specification, the ToPrimitive (AO) takes two arguments input and preferredType hint.

input is the value that needs to be converted to the primitive.

preferredType hint depends upon the type of operation we are performing. There are three types of hints "string", "number" or "default".

Since hint depends upon the type of operation we are performing. If we are performing the numeric operation and ToPrimitive (AO) get invoked it will send "number" as the hint.

HINT as "number"

[ ] + 1 ---------------> [ ] is non-primitive
ToPrimitive(input [, PreferredType]) ---------------> ToPrimitive invoked
ToPrimitive( [ ] , "number") ---------------> input = [ ] & hint = "number"
Output: [ ] + 1 --> 0 + 1 --> 1

In the above example we are performing a numeric operation, one of the operands is a non-primitive value, i.e, [ ].

JS converts this Non-primitive value to a primitive value implicitly. So under the hood, ToPrimitive is invoked.

ToPrimitive AO will receive "number" as a hint because we performing the numeric operation. So "number" as a hint will set preferredType as "number".

HINT as "string"
[ ] + "Vivek" ---------------> [ ] is non-primitive
ToPrimitive(input [, PreferredType]) ---------------> ToPrimitive invoked
ToPrimitive( [ ] , "string") ---------------> input = [ ] & hint = "number"
Output: [ ] + "Vivek" --> "" + "Vivek" --> "Vivek"

In the above example we are performing concatenation, one of the operands is a non-primitive value, i.e, [ ].

JS converts this Non-primitive value to a primitive value implicitly. So under the hood, ToPrimitive is invoked.

ToPrimitive(AO) will receive "string" as a hint because we are concatenation. So "string" as a hint will set preferredType as "string".

HINT as "default"

Whenever ToPrimitive(AO) invokes without a hint, it sends "default" as a hint. "default" means it will take whatever value type it is.


Now you would be saying what will happen after we set the preferredType hint, how the conversion happens of the input that gets passed in ToPrimitive(input [, PreferredType]).

There are two methods available on every object that are used to convert them to Primitive:
(1) valueOf()
(2) toString()

Depending upon the hint these two method's priority is decided:

  1. hint = "number"
    ToPrimitive(input, "number")
    First valueOf() and then toString()
    valueOf(input) --> If the returned value is a primitive it will use it otherwise return the value.
    The returned value from the valueOf() will be used in toString()
    If it gets a primitive value it will use it and still not get then it will result in type error.

  2. hint = "string"
    ToPrimitive(input, "string")
    First toString() and then valueOf()
    here the order is reversed
    toString(input) --> If the returned value is a primitive it will use it otherwise return the value.
    The returned value from the toString() will be used in valueOf().
    If it gets a primitive value it will use it and still not get then it will result in a type error.

  3. hint = "default"
    ToPrimitive(input, "number")
    same as in the case of hint as "number"

Another thing to note about algorithms in Javascript is that they are inherently recursive, this means that if ToPrimitive (AO) gets invoked and it doesn't return any primitive, it will keep getting invoked until it returns a primitive or an error in some cases.

ToNumber Abstract Operation:

ToNumber (AO) in javascript converts different value datatypes to a number.

So whenever we perform a numeric operation and one or both of the operands are numbers then the Javascript engine will invoke the ToNumber (AO) to convert them to a value of type "number".

The ToNumber (AO) takes an argument and converts it to a value of type "number" according to the following table in the specification:

In case of any primitive value(string, boolean, null, undefined...) conversion to number:

/* Primitive to Number */
console.log(3 - "2");  /*  1 */
console.log("10" * 2); /* 20 */

In JS there is an implicit type conversion, JS calls ToNumber(string) (AO) on "2"(string) and converts it to 2(number).

In case of conversion from any non-primitive value like object(array) to number:

console.log(3 - [2]);  /* 1 */
/* [2].toString() => "2" */
/* ToNumber (AO) is invoked and it will convert the "2" to 2.*/
/* So, 3 - 2 = 1

Here [2] should be a number as we have discussed above, anytime we need to do something numeric and we don't have a number, we're gonna invoke the ToNumber (AO). Also, here data type is an object which is a non-primitive value. So we will follow the following algorithm:

  1. In the above code numeric operation is being done, so JS will invoke ToNumber(argument).

  2. ToNumber will get an object as its argument ==> ToNumber(object).

  3. On receiving an object as its argument, ToNumber will further invoke ToPrimitive (AO).

  4. We know two hints are there a "number" or "string", here we will pass a "number" hint as we are performing a numeric operation.

  5. ToPrimitive(input, "number"), when a hint is a "number" will first start with valueOf() and then toString().

  6. valueOf() method just returns itself and then it calls the toString() method.

  7. toString() converts [2] to "2".

  8. Now the ToNumber (AO) receives that primitive value as an argument ToNumber("2").

  9. It will finally convert it to 2.

Now with the abstract operations discussed above we can start discussing "==" and "===".

Loose Equality Operation:

Also called Abstract Equality Comparison.

The above image is from the ECMAScript which is the source of truth, which means we can rely on the rules discussed above.

we will be discussing each rule one by one thoroughly.

const firstName = "Vivek";
console.log(typeof firstName); /* string */
const lastName = "Vivek";
console.log(typeof lastName); /* string */
console.log(firstName == lastName); /* true */

/* Data Types are same so under the hood they are compared using "===" 
strict equality */

In the above code, the Type(x) = Type(firstName) is "string" and Type(y) = Type(lastName) is "string". whenever the datatypes of both x and y is same, then they are further compared using "===" strict equality.

const valueOne = null;
const valueTwo = undefined:
console.log(valueOne == valueTwo); /* true */
console.log(valueTwo == valueOne); /* true */

const age = 20;
console.log(typeof age); /* "number" */
const name = "20";
console.log(typeof name); /* "string" */
console.log(age == name); /* true */
console.log(name == age); /* true */
/* According to the above specification if x == y, in which if Type(x) is 
Number and Type(y) is String and Vice-versa. Then under the hood, the 
string part will be converted to number by impicit type coercion by 
ToNumber abstract operation. So, "20" gets converted to 20*/

const toggle = true;
console.log(typeof toggle); /* "boolean" */
const age = 20;
console.log(typeof age); /* "number" */
console.log(toggle == age);  /* false   */
/* According to the above specification if x == y, in which if either 
Type(x) or Type(y) is Boolean and Vice-versa. Then under the hood, the 
Boolean part will be converted to number by impicit type coercion by 
ToNumber abstract operation. So, true gets converted to 1*/

const num = 2;
console.log(typeof name); /* "number" */
const list = [2];
console.log(typeof []); /* "object" */
console.log(name == list);  /* false   */
console.log(list == name);  /* true    */
/* According to the above specification if x == y, in which if Type(x) is 
(String, Number or Symbol) and Type(y) is object and Vice-versa. Then 
under the hood, the object type will be converted by impicit type 
coercion by ToPrimitive abstract operation to number or string based on 
the hint provided.
So by following the algorithm for ToNumber(object) in case of non-
primitive value:
(1) ToPrimitive(hint) with hint "number" is invoked.
(2) So, if number hint then first valueOf() then toString().
(3) We will ignore valueOf();
(4) Applying toString() on [2], we will get "2".
(5) Now our datatype is primitive(string), so "2" further get converted 
to 2 by ToNumber Abstract Operation in case of Primitive.

If our code does'nt follow all the above rules, then it will return false.

Strict Equality Comparison:

Between the loose and strict equality, strict equality is the most widely used in the code. The main reason for its high usage is due to lack of knowledge of people regarding the loose equality and people believe that strict equality always says truth. But let me clear this misconception of your's.

/* Rule 1: (When data types are different) */
const str = "3";  /* "string" */
const num = 3;    /* "number" */
console.log(str === num);  /* false */

/* As we can see that there is no conversion when data types are 
different in === strict equality. */
/*--------------------------------------------------------------------*/
/* Rule 2: (This rule includes some exceptions) */
/* (a) If either or both of the values are NaN then === equality will 
always return false.*/
/* Example 1: (Both the values are same)*/
const valueOne = NaN;
const valueTwo = NaN;
console.log(valueOne === valueTwo); /* false */

/* Example 2: (Either of the values are NaN)
/*This will obviously return false */

/*(b) In the case of -0 and 0. */
const negZero = -0;
const zero = 0;
console.log(negZero === zero);   /* true */
console.log(zero === negZero);   /* true */