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).
Table of Contents
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 isnull
. - 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
-
JavaScript’s Apply, Call, and Bind Methods are Essential for JavaScript Professional Must READ first !
- MDN : CLOSURES https://developer.mozilla.org/en/docs/Web/JavaScript/Closures
- What You Should Already Know about JavaScript Scope https://spin.atomicobject.com/2014/10/20/javascript-scope-closures/
- Chapter 3.1. Lexical environments: Common Theory http://dmitrysoshnikov.com/ecmascript/es5-chapter-3-1-lexical-environments-common-theory/#more-1751
- Chapter 3.2. Lexical environments: ECMAScript implementation http://dmitrysoshnikov.com/ecmascript/es5-chapter-3-2-lexical-environments-ecmascript-implementation/