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
- 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("----------------------------------------------------");
}
- 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
- 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
- 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
- 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
- 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
- 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