JavaScript under the hood - Scoping (part 2)
In my previous article about scoping. We noted that JavaScript (like most languages) employs a lexical scoping model - the rules for where variables and functions are accessible depends on where the code was written by the author of the code; i.e. these rules were set in stone during compile time and will not be changed. This article is based on the excellent PluralSight course called “Advanced Javascript” by Kyle Simpson
In this article we’ll look at how the scoping mechanism works in relation to the this object. In order to fully understand how scoping works in relation to this, it’ll be helpful to understand the basics of dynamic scoping, which is another paradigm used by some programming languages to resolve scope lookup.
Here’s a simple example of lexical scoping in action:
function funcOne() {
var foo = "bar";
testFuncTwo(); // call testFuncTwo, hoping somehow it will have access to foo
}
function funcTwo() {
console.log(foo); //Nope. throws ReferenceError: foo is not defined
}
testFuncOne();
When the JavaScript runtime asks the scope of funcTwo to provide the reference for foo, the scope manager is not able to find it in the scope of funcTwo, so it looks at where funcTwo was defined and searches THAT scope. In our example, funcTwo was defined in the global scope (which is the outer most scope in JavaScript). Well, there’s no identifier called foo in the global scope, so the program throws an error.
To drive the point home, look at the following code:
function funcOne() {
var foo = "bar";
function funcTwo() {
//<---funcTwo is defined inside of funcOne
console.log(foo); //outputs "bar";
}
testFuncTwo(); // call testFuncTwo, hoping somehow it will have access to foo (spoiler: it does)
}
testFuncOne();
In the example above, funcTwo was defined in the scope of funcOne, which is how funcTwo has access to foo.
Now if JavaScript was dynamically scoped, here’s how the following code would be parsed:
//Let's analyze this code via the lenses of dynamic scoping.
function funcOne() {
var foo = "bar";
testFuncTwo(); // call testFuncTwo, hoping somehow it will have access to foo
}
function funcTwo() {
console.log(foo); //outputs "bar".
}
testFuncOne();
When the JavaScript runtime asks the scope of funcTwo to provide the reference for foo, the scope manager is not able to find it in the scope of funcTwo, so it looks at where funcTwo was executed and searches THAT scope. In our example, funcTwo was executed from inside of funcOne, which has an identifier called foo.
Well, that’s dynamic scoping! The rules for where variables and functions are accessible depends on where the block of code was executed; and since our code is run during, well, run-time, the accessibility of variables/functions is determined during run time, as opposed to lexical scoping, which resolves all such rules during compile time.
The dynamic scoping model we just learnt is helpful in understanding one of the most powerful mechanism in JavaScript, the this object.
Every function while executing has a reference to its current execution context, called this. An execution context is an abstract concept used to define the notion that every context holds information about the variables/functions that are defined within it.
The scoping rules pertaining to this don’t follow the lexical model, but instead the dynamic scoping model in that the object that this will point to depends on the call site of the function, not where it was defined!
There are 4 rules we need to learn about how this gets bound in order to master it. Let’s learn about them with the following example:
//This function will get bound to the getName method
//defined on the personOne and personTwo objects
function getName() {
//line 1
return this.name;
}
var name = "John"; // line 2
var personOne = { name: "Jane", getName: getName }; //line 3
var personTwo = { name: "Ali", getName: getName }; // line 4
console.log(getName()); //line 5 outputs "John";
console.log(personOne.getName()); //(ine 6 outputs "Jane";
console.log(personTwo.getName()); //line 7 outputs "Ali";
When we execute the getName function on it’s own (line 5), the this object points to the global context; This is known as the default binding rule. In this case, the global context has a variable called name (line 2), whose value is “John”. Note that if we were in strict mode, this would be undefined and executing this line would throw a reference error, something like “Cannot read property ’name’ of undefined”.
When we execute getName as a method on an object, this points to the containing object. On line 3, where we defined the personOne object, we created a name property and assigned the value “Jane” to it. Note that the method getName points to the the function getName which was defined on line 1. Then on line 6, when we executed the method (personOne.getName()), the JavaScript runtime knew this was a method on an object, and searched that object for a variable called name, found it, and returned its value. The exact same process occurred on line 7 to output the value “Ali”. This is known as the implicit binding rule , that is, the this object points to the object calling the method.
Let’s imagine that we have a form and created an object which encapsulates all logic relating to the form.
//This function is defined in the global context, for illustrative purposes only..
function displaySuccessMsg() {
console.log("global context");
}
const FormValidator = {
//A very contrived method that handles form submission.
handlePost: function () {
this.postForm().then(function () {
//do some stuff with the response
this.displaySuccessMsg(); // logs global context whereas we expected it to log FormValidator context!
});
},
displaySuccessMsg: function () {
//handle success response here
console.log("FormValidator context");
},
//A helper method that simulates a network request.
//Not important for our purpose and you can ignore it
postForm: function () {
return Promise.resolve();
},
};
FormValidator.handlePost(); //simulate a post event.
Let’s analyze the code above. The only interesting bit of code is the handlePost method, which simply involves executing the postForm method and passing to it an anonymous callback function that is invoked once the promise returned by the postForm is resolved. Note that when the callback is invoked, we expect displaySuccessMsg to point to to the method invoked on the FormValidator object but instead it points to the displaySuccessMsg function in the global context! What’s going on ?! You may expect the the second rule we learnt, the implicit binding rule to apply because you may think that the containing object is FormValidator but instead, the default binding rule applied! Well, I mentioned earlier that the object this points to depends on its call site, and in this case, the call site was from within a brand new execution context created by an anonymous function. This anonymous function is NOT the containing object FormValidator, so the implicit binding rule doesn’t apply and thus the scoping rule is the default binding rule. Note that if we passed the displaySuccessMsg instead of an anonymous callback function, the this binding would’ve been correct.
this.postForm().then(this.displaySuccessMsg); //logs FormValidator context
In the above, the call site is postForm, which belongs to FormValidator, hence the this binding is as expected. Here’s how we ensure that this points to FormValidator in the original example(Hint:we use the bind keyword).
handlePost: function() {
this.postForm().then(function() {
//do some stuff with the response
this.displaySuccessMsg(); //logs FormValidator context
}.bind(this)); <--notice the bind function
},
The bind function essentially overrides the default object that this points to in favor of the object passed as its parameter. This is known as the explicit binding rule, as in, we explicitly set what object this points to. Here’s another example to drive the point home:
function getFullName() {
console.log(this.firstName + " " + this.lastName);
}
var personOne = {
firstName: "John",
lastName: "Doe",
getFullName: getFullName,
};
var personTwo = {
firstName: "Jane",
lastName: "Doe",
getFullName: getFullName,
};
personOne.getFullName.bind(personTwo)(); // outputs Jane Doe
I would be remiss if I didn’t mention call and apply, which are helper functions that do pretty much the same thing as bind, the only difference being that both call and apply require that the function be called immediately. So you can do the following with the bind function:
if (condA) testFunc.bind(objA);
else if (condB) testFunc.bind(objB);
else testFunc.bind(objC);
testFunc(); //Note how the process of binding the function is divorced from executing it.
//This is not possible with "call" or "apply"
If you’re coming from an object oriented language you may think that the new keyword is used to instantiate classes. This is not true in JS. The new keyword doesn’t instantiate anything. Also, JavaScript doesn’t have classes (the class keyword introduced in ES6 is just a special type of function).
function Person(name, age) {
this.name = name;
this.age = age;
}
var personOne = new Person("John", 25); //<-- notice the 'new' keyword
Putting the new keyword in front of any function changes that function from a function call to a construction call. It does 4 things (besides executing the function)
1.Creates a brand new empty object.
2.This empty object gets linked to it’s own prototype object (this will be explored in depth in an upcoming article).
3.The brand new object gets bound to the this object (basically something like this = {}) (Once this step is completed, the body of the function is executed).
4. If the function doesn’t return anything, the new object is returned.
This is the forth way the this keyword can be bound; it can be bound to a brand new object that was created as part of the constructor call hijacking.
Now that we’ve learnt the four rules needed to determine what value this resolves to, note that these rules are applied in the reverse order of their precedence, i.e. the runtime first determines whether the containing object is a function construction call, if that’s not applicable, than the runtime looks at whether the function’s execution context was bound to another object, if that’s not applicable, than the runtime looks at whether the variable has an implicit binding, if that’s not applicable either, the default binding rule is applied.
We learnt that JavaScript employs lexical scoping to determine where to look for variables, but when accessing variables that are bound to the this object (e.g. this.foo), the process it employs is a lot closer to dynamic scoping. Note that under the hood, it’s still lexical scoping, it’s just that analyzing the code through the dynamic scoping paradigm makes JS behavior more digestible. What happens is that when the JavaScript engine comes across the this keyword as it’s parsing the code, it will bind this as an invisible zeroth argument to the enclosing function, so in a way, you can think of this as just another parameter passed to the function, whose value is resolved at runtime!
Now that we have a solid understanding of the internals of the scoping mechanism, we can leverage them to write cleaner, more maintainable and more powerful JavaScript! Stay tuned to find out how!