Understanding Javascript Quirks

Anupama H

@anuhosad

(Coercion, Equality & Relational Comparison, Hoisting)

  • Implicit Type Coercion for operators
    • ToNumber
    • ToString
    • ToBoolean
    • ToPrimitive
  • Equality Comparison
  • Relational Comparison
  • Hoisting

What we are going to cover...

@anuhosad

Type Coercion for Operators

@anuhosad

[] + {} 
"[object Object]" 
{} + []
0
true + true
2
true / false
Infinity
false / true
0
[] * {}
NaN
{} * []
Uncaught SyntaxError: Unexpected token '*'
[] * []
0
["8"] / "2"

@anuhosad

["8","2"] / "2"
NaN
4

@anuhosad

Javascript Values

Primitive Values

Non-Primitive Values

  • number
     
  • string
     
  • boolean
     
  • null
     
  • undefined
  • Object
     
  • Array
     
  • Function

@anuhosad

Operator

Behaviour

<value1>  +  <value2>

If operands are numbers, add

If operands are strings, concatenate

All other values converted to number or string

<value1>  -  <value2>

Performs subtraction for number

Other values converted to number

<value1>  *  <value2>

Performs multiplication for number

Other values converted to number

<value1>  /  <value2>

Performs division for numbers

Other values converted to number

@anuhosad

Primitive Value operation algorithms

@anuhosad

ToNumber

Argument Result
undefined NaN
null 0
boolean true is converted to 1,
false is converted to +0
number no conversion necessary
string parse the number in the string. for example, "324" -> 324
object primValue = ToPrimitive(v, hint Number)
return ToNumber(primValue)

@anuhosad

ToString

Argument Result
undefined "undefined"
null "null"
boolean true is "true",
false is "false"
string no conversion necessary
number the number as a string, e.g. "1.765"
object ​primValue = ToPrimitive(v, hint String)
return ToString(primValue)

@anuhosad

ToBoolean

Argument Result
undefined false
null false
boolean no conversion necessary
string "" is false, else true
number false if +0, -0 or NaN
else true
object true

@anuhosad

true + true
true / false 
null + undefined
null + null

@anuhosad

"12" + 9 - 4 
= 1 + 1
= 2
= 1 / 0
= Infinity
= "129" - 4
= 125
= 0 + NaN
= NaN
= 0 + 0
= 0

All these works on Primitive values

 

But, what if the operands are
Non-Primitive values ?

@anuhosad

ToPrimitive

 ToPrimitive(input, PreferredType?)

 

  1. If input is primitive, return it as is
     
  2. Otherwise, input is an object. Call obj.valueOf(). If the result is primitive, return it
     
  3. Otherwise, call obj.toString(). If the result is a primitive, return it
     
  4. Otherwise, throw a TypeError

 


 

 

  Ref: https://www.ecma-international.org/ecma-262/5.1/#sec-9.1

@anuhosad

=> PreferredType is either Number or String, default is Number

=> Above is assuming PreferredType = Number, if PreferredType = String, steps 2 & 3 are swapped

=> If PreferredType is missing then is set to String for Date instances & to Number for all other values

var arr = [];

arr.valueOf() === arr

var obj = {};

obj.valueOf() === obj

var arr = [];

arr.toString() === ""

var obj = {};

obj.toString() === "[object Object]"

@anuhosad

var obj = {
    valueOf: function () {
        console.log("valueOf");
        return {}; // not a primitive
    },
    toString: function () {
        console.log("toString");
        /* return the meaning of life! */
        return 42;
    }
};

console.log(obj * 2);  // 84

ToPrimitive (contd..)

var obj = { valueOf: function () { return 2 } };

console.log(6 + obj); // 8
var obj = { toString: function () { return "def" } };

console.log("abc" + obj); // abcdef

@anuhosad

[] + {} 
  [].toString() + {}.toString()
"" + "[object Object]"
"[object Object]"
  [].valueOf() + {}.valueOf()

@anuhosad

{} + [] 

  +[]
  +""
   0

Code Block

+[].toString()
Number("")
+[].valueOf()

@anuhosad

{} + {}

 

 +{}

Code Block

 +"[object Object]"
 NaN
+{}.toString()
+{}.valueOf()

@anuhosad

({} + {}) 
"[object Object][object Object]"

Parenthesis prevents {} being treated as a code block

@anuhosad

[200] + 100 
  [200].toString() + 100
"200" + 100
"200100"
  [200].valueOf() + 100

@anuhosad

[200] / 100 
  [200].toString() / 100
"200" / 100
= 2
  [200].valueOf() / 100

@anuhosad

200 / 100
+[100, 200] 
  +[100, 200].toString()
+"100,200"
NaN
  +[100,200].valueOf()

@anuhosad

[] * {}
{} * []
[] * [] 
[] / true
(function(){return 100}) + 100
= "function(){return 100}100"

@anuhosad

= "" * "[object Object]"
= NaN
= *[]
= Syntax Error
= "" * ""
= 0 * 0
= 0
= "" / 1
= 0 / 1
= 0

Equality Comparison

[] == 0
= true 

0 == null
= false 

new String("abc") == "abc"
= true 

[1,2,3] == 123
= false 

[123] == 123
= true 

@anuhosad

new String("abc") === "abc"
= false 

    String("abc") === "abc"
= true 

Abstract Equality Comparison Algorithm

@anuhosad

1. undefined == undefined

Abstract Equality Comparison (contd..)

@anuhosad

2. null == null

3. NaN != NaN

4. both objects -> compare references

5. null == undefined & undefined == null

6. string -> convert to number

7. boolean -> convert to number

8. object -> convert to primitive (hint: number)

[] == 0
= true
[].valueOf() == 0
[].toString() == 0
"" == 0
0 == 0

@anuhosad

[1,2,3] == 123
= false 

ToPrimitive([1,2,3]) == 123
ToNumber("1,2,3") == 123
NaN == 123
"1,2,3" == 123

@anuhosad

[123] == 123
= true 

ToPrimitive([123]) == 123
ToNumber("123") == 123
123 == 123
"123" == 123

@anuhosad

[] == ![]
= true 

[] == false
0 == false
0 == 0
"" == false

@anuhosad

const a = [1, 2, 3]
const b = [1, 2, 3]
const c = "1,2,3"

console.log(a == c)

console.log(a == b)
= true
new String("abc") == "abc"
= true 

0 == null
= false 

@anuhosad

new String("abc") === "abc"
= false 

("1,2,3" == "1,2,3") 
= false
    String("abc") === "abc"
= true 

Strict Equality Comparison Algorithm

@anuhosad

Relational Comparison Algorithm

x < y is compared as follows

@anuhosad

1. Convert the operands to primitive using ToPrimitive (hint:  Number) algorithm (for whichever once is not primitive)

2. If both operands are strings, compare their character codes

3. If both operands are not strings, convert the non-string operand to number using ToNumber() algorithm and compare

"being honest" > "being encouraging"
= true 

"human" < "Machine"
= false

"12" > "3"

= false

"10000" > "abc"

= false

100 > "50"

= true

@anuhosad

"human" < "machine"
= true

Hoisting

Hoist = raise (something) by means of ropes and pulleys

Conceptually,  hoisting suggests that variable and function declarations are physically moved to the top of your code

Technically, the variable and function declarations are put into memory during the compile phase, but stay exactly where you typed them in your code.

@anuhosad

Hoisting (contd...)

A function declaration is hoisted along with its definition

This lets us use it before it is declared in the code

Only variable declarations are hoisted but not their definitions

A function expression is not hoisted

Hence you cannot use it before it is declared

@anuhosad

Function Declaration vs Expression

=> function foo () {}

=> var foo = function () {};

Declaration

Expression

=> Can call before it is                        declared

=> Only declaration is               hoisted

=> Both declaration and                 function body are hoisted

=> Cannot call before it is                declared

@anuhosad

var a = 20;

function foo() { 
   console.log(a);
   var a = 10;
}

 

foo();

Output : undefined
var a = 20;

function foo() { 
   var a;
   console.log(a);
   a = 10;
}

foo();

@anuhosad

var a = 20;

function foo() { 
   console.log(a);
   var a = 10;
   return;
   function a() {}
}

 

foo();

Output : f a() {}
var a = 20;

function foo() { 
   var a;
   function a() {}
   console.log(a);
   a = 10;
   return;
}

foo();

@anuhosad

var a = 20;
 

function foo() {
   console.log(a);
   var a = 10;
   return;
   var a = function() {};
}


foo();

Output : undefined

var a = 20;
 

function foo() {
   var a;
   console.log(a);
   a = 10;
   return;
   a = function() {};
}


foo();

@anuhosad

var a = 1;

function b() {
  a = 10;
  return;
}

b();

console.log(a);
Output : 10

@anuhosad

var a = 1;

function b() {
  a = 10;
  return;
  function a() {}
}

b();

console.log(a);
Output : 1
var a = 1;

function b() {
   function a() {}
   a = 10;
   return;
}

b();

console.log(a);

@anuhosad

function parent() {
    var foo = "variable";

    function foo() {
        return 42;
    }

    return foo(); 
}

console.log(parent());
Output : Uncaught TypeError:
            foo is not a function
function parent() {
    function foo() {
        return 42;
    }

    foo = "variable";

    return foo(); 
}

console.log(parent());

@anuhosad

function parent() {
   return foo();
   function foo() {
       return 42;
   }
   var foo = "variable";
}

console.log(parent());
Output : 42
function parent() {
   function foo() {
       return 42;
   }
   var foo; // ignored
   return foo();
   foo = "variable";
}

console.log(parent());

@anuhosad

var abc = 0;

if (1 === 0){
    function a(){
        abc = 7;
    }
} else if ('a' === 'a'){
    function a(){
        abc = 19;
    }
} else if ('foo' === 'bar'){
    function a(){
        abc = 'foo';
    }
} 

a();
console.log(abc);
Output : 19

 In an ES2015 environment, a function declaration inside of a block will be scoped inside that block

@anuhosad

let a = 20;
{

        console.log(a);

        console.log(typeof(a));

        let a = 10;

}
Uncaught ReferenceError: Cannot access 'a' before initialization

Temporal Dead Zone

Hoisting & "let"

@anuhosad

 var a = 20;
{

        console.log(a);

        console.log(typeof(a));

        var a = 10;

}
// 20
// number

var a = 20;

function foo() {

         console.log(a);

         console.log(typeof(a));

         var a = 10;

}

foo() 

// undefined
// "undefined"

@anuhosad

References

@anuhosad

@anuhosad

Understanding Javascript Quirks

By anupama hosad

Understanding Javascript Quirks

  • 1,248