JavaScript: Tackling THIS Object, Nested Functions/Closures by Samples

Overiew  – Some key Facts You Should Know

  • JavaScript has lexical scoping with function scope.
  • JavaScript looks like it should have block scope because it uses curly braces { },
    BUT a new scope is created only when you create a new function !
  •  JavaScript loses scope of THIS when used inside of a function that is contained inside of another function [ Nested Function, Closure ]
  • When Scope gets lost, by default, THIS will be bound to the global window object
  • The way  lexical scoping works in JavaScript works can’t be modified
  • Only the control of the context in which functions are called can be modified
  • Nested Function Definition : The nested (inner) function is private to its containing (outer) function. It also forms a closure.
  •  A closure is an expression (typically a function) that can have free variables together with an environment that binds those variables (that “closes” the expression).

This and Using String Literals for Object Creation

var john1 = 
{
    firstName: "John",
    sayHi:     function()
    {
        console.log("  Hi " + this.firstName + " !" );
        checkOBJ(this);
    }
};

function runThisTest1()
{
    console.log("--- Testing Object Literal with Dot Notation: john1.sayHi()  ---");
    john1.sayHi();
    console.log("--- Testing Object Literal with Bracket Notation:  john1['sayHi']() ---");
    john1['sayHi']();
    console.log("----------------------------------------------------");
}

Console.log 

js_Obj1

  • JavaScript methods can be called with Dot and Bracket Notation
  • The Constructor of Objects created via  Object Literals is the Object Type

This and Objects created with the NEW Operator

function User(name) 
{
    this.name = name;
    this.sayHi = function() 
    {
        console.log(" Hi I am "  + this.name);
        checkOBJ(this);
    };
}

function runThisTest3()
{
    console.log("--- New Operator:  var john = new User('John'); ---");
    var john = new User("John");
    john.sayHi();
    console.log("----------------------------------------------------");
}

js_Obj2

  • The constructor for our john object is the  User() Type
  • __proto__ points onto the Object type   [  == Prototypal inheritance ]

–> So far nothing is very special and things are straight forward

Understand the Lexical Environment – This step is needed for NESTED FUNCTIONS

var i = 99; 

var createAdders = function() 
{
    var fns = [];
    for (var i=1; i<4; i++) { 
        fns[i] = (function(n) {
            return i+n;
        });
    }
    i = 10;
    return fns;
};

function runThisTest5()
{
    console.log("--- Lexical Envrironment gives us Strange Results --- ");    
    var adders = createAdders();
    for ( var i = 1; i<4; i++  )
    {
        console.log("Result: " + adders[i](7)  + " - Function  adders[" + i + "] : " + adders[1] ); //17 ??
    }
    console.log("----------------------------------------------------");
}

Console.log

js_Obj3

  • Function createAdders() creates 3 different Adder Functions
  • All of these function references variable i – no obvious erorr
  • You may expect that these functions return 8,9 and 10 but they don’t do that
  • Instead all of these three functions returns 17 Why ?

Lexical environments

  • When a function is called, an NEW Lexical environment is created for the new scope that is entered.
  • Each new Lexical Evironment has a field called outer that points to the outer scope’s environment and is set up via [[Scope]]
  • There is always a chain of environments, starting with the currently active environment, continuing with its outer environment
  • Every chain ends with the global environment (the scope of all initially invoked functions). The field outer of the global environment is null.
  • An environment record records the identifier bindings that are created within the scope of this lexical environment
  • That is, an environment record is the storage of variables appeared in the context
  •  A lexical environment defines the association of identifiers to the values of variables and functions based upon the  lexical nesting structures of ECMAScript code.
globalEnvironment = { 
  environmentRecord: {
    // built-ins:
    Object: function,
    Array: function,
    // etc ...
    // our bindings:
    i: 99
  },
  outer: null // no parent environment
};

// environment of the "runThisTest5" function
runThisTest5 = {
  environmentRecord: {
    i :   1 [ 2,3 ] 
    adders:  Reference  to createAdders fn[]
  },
  outer: globalEnvironment

// environment of the "createAdders" function 
createAdders = {
  environmentRecord: {
    i: 10
    fn[] = [1] function (n) { return i+n; }
           [2] function (n) { return i+n; }
           [3] function (n) { return i+n; }
  },
  outer: runThisTest5
};

     Note:

  • After Function creation i = 10 gets executed and this data is saved in our LEXICAL Function Environment before returning the function array
  •  Later on the Functions “createAdders[i]” where executed and >> i << gets resolved from our LEXICAL Function Environment where i=10 !
  • This is the reason why all of our functions returns the same result
  • As we have different lexical Environments for every new function the THIS scope changes too !
  • Nested Functions are one well know sample for this behavior .

This and Nested Functions a potential problem

var catname = "Im a REAL PROBLEM";
function Cat6 ( name, color, age)
{
   this.catname = name;
   this.color = color;
   this.age = age;
   this.printInfo =  function() 
   {
      var that = this;
      nestedFunction = function() 
      {
         console.log("   Object Properties: Name:", this.catname, " - Color:", this.color, " - Age:", this.age );
         checkOBJ(this);
      }; 
      nestedFunction2 = function() 
      {
         "use strict";
        try
        {
            console.log("   Object Properties: Name:", this.catname, " - Color:", this.color, " - Age:", this.age );
        }  catch ( err )
        {
            console.log(" Error getting Object details: Error : " + err );
        }
      };
      
      nestedFunction3 = function() 
      {
         console.log("   Object Properties: Name:", that.catname, " - Color:", that.color, " - Age:", that.age );
         checkOBJ(that);
      }; 
      
      console.log('--- Call nestedFunction() - Not VALID THIS object generates a huge PROBLEM ---');
      nestedFunction();  
      console.log("----------------------------------------------------");
      console.log('--- Call nestedFunction() using a saved THIS Context armed by "use strict"; ---');
      nestedFunction2();
      console.log("----------------------------------------------------");
      console.log('--- Call nestedFunction() using a saved THIS Context ---');
      nestedFunction3(); 
    };
};

function runThisTest6()
{     
    console.log('--- THIS behavior with Nested Functions and NEW Operator  [ test6 ] ---');    
    var myCat6 = new Cat6('Fini', 'black', 7);
    myCat6.printInfo();   
    console.log("----------------------------------------------------");
}

 

Console.log

js_Obj4

  • nestedFunction()  has lost the original THIS context and is working with Window context
  • This can be quite dangerous as we may pick up wrong object properties [ like this.catname from the window object ]. All other properties become undefined.
  • nestedFunction2()  shows how we can detect this error by using “use strict”;  directive
  • nestedFunction3() shows a solution for the problem by storing  the current this object reference in the lexical Environment [ var that = this; ]. Later on this object reference is used to read object details.

THIS behavior with Nested Functions and NEW Operator using call(), apply(), bind()

  • To Fix the problem with an INVALID THIS context use call(), apply(), bind()
var name = "This SHOULD NEVER be printed !";
function Cat9 ( name, color, age)
{
   this.name = name;
   this.color = color;
   this.age = age;
   this.printInfo =  function() 
   {
      
      console.log("   Contructor Name:", this.name, " - Color:" + this.color, " - Age:" + this.age );
      nestedFunction = function() 
      {  
        // Window object alway will raise Error : caught TypeError: Converting circular structure to JSON 
        console.log("   Object Properties: Name:", this.name, " - Color:", this.color, "- Age:", this.age  );
        checkOBJ(this);
      };
      
      console.log('--- Using call() by providing this context as first parameter ---' );
      nestedFunction.call(this,  'Using call() by providing this context as first parameter' );
      
       console.log('--- Using apply() by providing this context as first parameter ---');
      nestedFunction.apply(this, []);
 
      var storeFunction = nestedFunction.bind(this);
      console.log('---  Using bind to save this context ---');
      storeFunction();
      
      console.log('---  Call nestedFunction() - SHOULD FAIL with undefined as THIS points now to WINDOW object---');
      nestedFunction();      
    };
};

function runThisTest9()
{   
    console.log('--- THIS behavior with Nested Functions and NEW Operator using call(), apply(), bind() [ test9 ] ---');    
    var myCat9 = new Cat9('Fini', 'black', 7);
    console.dir(myCat9);
    myCat9.printInfo();   
    console.log("----------------------------------------------------");
}

Console.log

js_Obj5

  • Using call(), apply() and bind() fixes the problem with a lost THIS context
  • The last test stresses again the fatal error that can happen when loosing THIS context

Real Sample: Borrow an Object Function Methode via call(), apply()

function Adder2 () 
{
    this.add = function(a,b) {  return a + b;  };
}

function runThisTest11()
{     
    var adder2 =  new Adder2;
    var res = adder2.add(2,3);
    console.log("--- Borrow an Object Function Methode via call(), apply()");
    console.log("    result of original Adder  created by NEW operator: " + res);
    
    console.log("    Adder called via call(): " + adder2.add.call( this, 2,3));
    console.log("    Adder called via apply(): " + adder2.add.apply( this, [2,3] ));
    console.log("----------------------------------------------------");
}

Console.log

js_Obj6

  • In this sample we borrow the add() function from the adder2 object by using call(), apply()

Real Sample: Using bind() within an AJAX request

function AjaxLoad2(url) 
{
    this.url = url;
    this.loadDoc = function(topLevelObject) 
    {
      var xhttp = new XMLHttpRequest();
      console.log("--- AJAX testing with bind() : Loading HTML page - URL: " + this.url);
     
      xhttp.onreadystatechange = function() 
        {        
         // When readyState is 4 and status is 200, the response is ready:
        // checkOBJ(this);
        if (xhttp.readyState === 4 && xhttp.status === 200) 
        {
            console.log("Return from AJAX Request:: HTML Page Loaded - URL: " + this.url +  " - status: " + xhttp.status);  
        }
        else if (  xhttp.readyState === 4 && xhttp.status === 0 )
        {
          console.log("Return from AJAX Request:: Error Loading Page - URL: " + this.url +  " - status: " + xhttp.status + " - readyState " + xhttp.readyState);  
          checkOBJ(this);
        }
      }.bind(topLevelObject);
           // .bind(this); should work too 
      
      xhttp.open("GET", url, true);
      xhttp.send();
    };
}

function runThisTest13()
{  
    /* 
     * Note: This Ajax request is expected to fail with  
     *        XMLHttpRequest cannot load https://google.com/. 
     *        No 'Access-Control-Allow-Origin' header is present on the requested resource. Origin 'http://localhost:8080' is therefore not allowed access.
     */
console.log("--- Real Sample using bind() in an AJAX request --- ");    
var ajax = new AjaxLoad2("https://google.com");    
ajax.loadDoc(ajax);
console.log("----------------------------------------------------");
}

Console.log

js_Obj7

  • The XHR failure is expected
  • .bind(topLevelObject);  line allows us to pick up the URL via this.URL when printing the error text

 Function checkOBJ() to display Object Details

function checkOBJ(origThis)
{
    var construtorName = origThis.constructor.name;
    var objectDetails = "";
    try
    {
        objectDetails = JSON.stringify(origThis);
    }  catch ( err )
    {
       objectDetails = " Error to stringify Object " + err; 
    }
    console.dir(origThis); 
    console.log("Constructor Name: " + construtorName + " - this Object Details: " + objectDetails);
}

Reference

Leave a Reply

Your email address will not be published. Required fields are marked *