Javascript Fundamentals 2
This is a continuation of the Javascript Fundamentals series, this time covering variable environments, scope, and 'this' in Javascript.
Variable environment
With each function creating an execution context, and each execution context having its own variable environment, it is important to know which variable environments we have access to when writing code.
Here is an example of variable environments and access:
function two() {
var isValid;
}
function one() {
var isValid = true;
two();
}
var isValid = false;
one();
In this example when code is running, a breakdown of our stack and the variable that will be accessed within each environment is as follows:
//two() -- isValid undefined (local)
//one() -- isValid true (local)
//global() -- isValid false (global)
Scope
This brings us to the concept of scope, which also brings us back to the concept of a Lexical environment. The scope of an execution context in javascript is determined lexically, or more simply, where the function has been written. in some other languages, access to data and variables is defined not by lexical scope (sometimes called static scope), but rather by dynamic scope, meaning that in those languages, the available variables are determined by where the function is called. When all of our functions are written in the global environment, they all have a link, or what we call the 'scope chain', which gives us access to variables that are in the global environment. In our browser, as mentioned earlier, this global environment would be the browsers 'window' object. We can however write our functions in a way to have what we call a function lexical environment. This makes our functions have a scope chain to each other rather than to the global environment. for example:
function sayMyName() {
var a = 'a';
return function findName() {
var b = 'b';
// console.log(c) // if we were to try to console log for the var c here, we will get a reference error, as this scope does not have access to any contexts below it
return function printName() {
var c = 'c';
console.log(a, b, c); // this function can access any variables above it
return 'Tim'
}
}
}
sayMyName()()();
In this example, we can see that our functions in this circumstance can access variables above their own context, but they cannot access variables defined below themselves. This is because of their position in the call stack. The execution contexts in the call stack can only access their own environment or below themselves in their scope. The scope chain will go through each outer scope in order until it reaches the global scope. With the above example, if we were to run it in our browser and find the function in our 'window' object, we would see that the function sayMyName has [[scopes]] property, also known as our scope chain. This property shows our scopes. It would look something like this:
{
[[Scopes]]: Scopes[2]
0: Script {LoadTimeData: ƒ}
1: Global {window: Window, self: Window, document: document, name: '', location: Location, …}
}
Looking even deeper into scope in javascript, when we're using function expressions we're actually creating a new execution context inside a variable environment, and it will not be seen inside the scope chain - meaning we cannot invoke this function directly.
var hi = function hello() {
return 'hello!'
}
hi(); // runs our 'hello' function and returns 'hello!'
//hello(); // throws a reference error because the hello function is not defined in our scope.
If we were to try to define a variable inside a function that doesn't exist, we should normally expect a reference error. In javascript however, in the following example we will not get any errors. This is another interesting and weird quirk of javascript.
function getHeight() {
height = 50; // because this hasn't been given a var, or const, or any other variable assignment, the javascript engine will assign it as a global variable.
return height
}
getHeight();
In the example, because height is not given any variable assignment, it will keep looking upwards in scope until it reaches the global context. In the global context, since it is not defined, the javascript engine will actually define it for us as a global variable. We call this 'leakage of global variables'. In older javascript, this could cause a lot of problems. One solution in modern javascript (ES5 onwards), that we have is the strict mode option ('use strict'). If we put 'use strict' on the top of our code file, it will throw a reference error in our example instead of defining the variable in the global scope for us.
Function Scope and Block Scope
In programming, scope simply means what variables and data we have access to within our functions. In javascript, we have function scope. Every time we run a function we create an execution context which has its own variable environment. Many other programming languages such as C or C++ determine scope differently, and use block scope. This means that the variables declared in a block are only accessible within that block, and not outside of it, even if it is within the same function. If we were to write something like this in C++:
{ // start of block and scope
int x = 100;
{ // start of new inner block and scope
int x = 200;
std::cout << x; // output is 200
} // end of inner block and scope
std::cout << x; // output is 100
} // end of block
In this instance, the int x in each declaration is a new variable, and even if they were given different names would not be accessible outside of their own blocks. This is very different to the function scope of Javascript, where if we declare a variable within a new block, it is still accessible outside of that block provided it is still within the same function.
if (100 > 1) {
var x = 100;
}
console.log(x); // this is accessible despite being declared inside a separate block
Many people regard block scoping as easier to read and generally a less error prone practice. Because of this, with ES6 in javascript we can now actually use block scoping by using 'let' or 'const' instead of var. These are block-scoped alternatives for variable declaration. So whenever we use let or const we are using block scoping, and so we can recreate the same C++ code above in javascript as follows:
{
const x = 100;
{
const x = 200;
console.log(x); // output is 200
}
console.log(x); // output is 100
}
This is especially useful for controlling scope within conditionals and loops. But knowing this, does it mean we should never use var? Generally speaking, yes. For the most part in not only web development, but javascript development as a whole you can just stick to const and let, but if you're ever working on or reading old code you will certainly be exposed to var so it is important to understand how it works.
IIFE
Before we had ES modules and module bundlers with modern javascript, whenever we were using various scripts and libraries on websites there could be a huge problem if scripts had conflicting names for functions and variables. In order to prevent global namespace pollution and we would use Immediately Invoked Function Expressions (IIFE). This is a function expression in javascript that looks something like this:
(function() {
})();
This is a common design pattern used by older libraries such as JQuery which allows us to have all of our library code inside a local scope to prevent any namespace collisions, such as variables or functions with the same name. This is simply using an anonymous function expression that we immediately invoke. All data and variables declared inside the expression is going to be scoped inside the expression, so it won't be accessible in the global environment. // With this pattern, we're able to avoid many name conflict.
var script1 = (function() { // declare all of script functions and variables, only adding the 'script1' variable to our global namespace
function getValue() {
return 10;
}
return { // return any values we need to be accessible outside of our scope
getValue: getValue
}
})();
var getValue = function() { // we can still have the same name for variables, functions, and expressions
return 5;
}
script1.getValue(); // returns 10
getValue(); // returns 5
We can also pass parameters into our scripts this way, which allows us to give them access to other libraries and scripts in the local scope instead of global, which will technically give a minor performance benefit. Doing this, we can also overwrite data on other scripts just for the local context if desired.
'This' Keyword In Javascript
One of the more confusing elements of javascript we regularly come across is the 'this' keyword. Due to its confusing nature, some developers even try to avoid using it whenever possible. This keyword can be explained as: "the object that the function is a property of". We can think of it as if it was a 'who called me?' keyword, as it will return whichever object called the function. So every function (every execution context) in javascript will have its own 'this' keyword to refer to its parent object. To give an example: If we were to open our browsers console and type 'this' we would get our window object. If we were to also create a new function in our console, and then log the value of 'this', what do you think we'd get?
function myFunction() {
console.log(this);
}
myFunction();
The answer in this case is we'd get our window object. This is because whenever we are calling this function defined in the global namespace, it is creating that function as a property of this global object. In most cases, we don't actually want to refer to the global object when using the 'this' keyword, which is one of the main pitfalls is. One of the ways to prevent us from referring to the window object in our browsers is strict mode. In this case, we would return 'undefined' instead. Strict mode is on by default in ES6 onwards.
const obj = {
name: 'Bob',
greet() {
return 'hello ' + this.name // this is the same as if we were typing obj.name. We can use 'this' to refer to any values inside obj
},
}
obj.greet(); // since we are calling object.greet(), we can think of the 'this' keyword of our method as pointing directly to the left over our dot, or as we stated earlier, the object that our function is a property of.
'This' keyword is useful because it gives our methods access to their object, allowing them to use access to its properties and methods. This means we can also execute the same code for multiple objects very easily. In the following example, what do you think we will have returned to us in each of the logs?:
const a = function() {
console.log('a', this)
const b = function() {
console.log('b', this)
const c = {
hi: function() {
console.log('c', this)
}}
c.hi()
}
b()
}
a()
The answer is that both a and b will give us the window object, whereas c will give us the 'c' object. This should make sense as a() is being called by the window object. The b() function is also being called by the global object, as there is nothing to the left of our b() call. This may initially confuse some people because if we look through our window object, we'd notice that b() is a property of our window.a() object, but our call is as if we were calling window.a(b()), so we can see that to the left of the dot is still our window object. In contrast, when we call c.hi(), we are calling the function through our c object, as if we were calling window.a(c.hi()), so to the left of our functions dot is our c object;
Let's look at another example.
const obj = {
name: 'Bob',
greet() {
console.log('a', this);
var myFunc = function() {
console.log('b', this)
}
myFunc()
}
}
obj.greet()
What do you think these logs will print? Maybe you guess that neither of them refer to the window object. Interestingly, our a will refer to our 'obj', but b will give us the window object. This is where things can be very confusing in javascript. This is because everything in javascript is lexically scoped aside from the 'this' keyword which is dynamically scoped, meaning it doesn't matter where it is written in the code - but rather how it is called.
What actually happens in this example is that the 'this' keyword defaults to the global object. So under the hood looks as if we were simply calling it from the global object since it is 'obj.greet()' calling the function. How do we avoid this tricky problem? With ES6 we can actually fix this by using arrow functions, as they are lexically bound - so unlike normal functions, arrow functions 'this' keyword is lexically scoped rather than dynamically scoped. Our above example will then look like this:
const obj = {
name: 'Bob',
greet() {
console.log('a', this);
var myFunc = () => {
console.log('b', this)
}
myFunc()
}
}
obj.greet()
Our output will no longer give us the window object for our 'b' log. It is instead pointing to where it was written, not how it is being called. This makes things much less confusing, especially for Object Oriented Programming.
One of the nicer, older solutions to this problem is to make a reference to our object and pass it into our functions rather than directly using 'this'. For example:
const obj = {
name: 'Bob',
greet() {
console.log('a', this);
var self = this;
var myFunc = () => {
console.log('b', self)
}
myFunc()
}
}
obj.greet()
This solution is slightly more verbose, but works very well. Another way this could be solved which was used in the past is to bind our function. That solution would look something like this:
const obj = {
name: 'Bob',
greet() {
console.log('a', this);
var myFunc = function() {
console.log('b', this)
}
return myFunc.bind(this)
}
}
// obj.greet() // if we run greet like normal in this case it will return our function as an object rather than call it
obj.greet()() // call 'myFunc' inside 'greet'
This can be a little more confusing to use and read, as well as even more verbose. Arrow functions are a very good solution that is simpler and easier to use, as well as read and understand.
Three very important ways we can manipulate the 'this' keyword in javascript are:
call()
apply()
bind()
// we've already seen bind previously
Let's have a look at these three functions and how to use them. Under the hood, all function invocations actually use 'call()'. If we did:
function hi() {
console.log('hi');
}
hi.call();
//hi(); // actually just a shorthand for hi.call()
hi.apply();
We'll notice the function runs as if we called it normally. It also looks like the same happens when we call apply. If we have a look at the description for call however, we'll see that it states: "Calls a method of an object, substituting another object for the current object". We then have two parameters, the first being the object we want to be used as the current object ('this'), and then any other arguments. Knowing that, it means we can actually share the same code that uses 'this' between different objects. Apply is actually very similar, the key difference that it takes an array of parameters so the formatting is a little different. Let's log a silly scenario to show how these can be used:
const warrior = {
name: 'Bob',
health: 100
}
const rogue = {
name: 'John',
health: 50
}
const wizard = {
name: 'Adam',
health: 50,
heal(amount) {
return this.health += amount;
}
}
console.log('first ', warrior);
console.log('first ', rogue);
wizard.heal.call(warrior, 50);
wizard.heal.apply(rogue, [50]);
console.log('second ', warrior); // health is now 150
console.log('second ', rogue); // health is now 100
As we can see, in this example the wizard is able to use his heal method on both the warrior and the rogue with call, and with apply. Bind is similar to both call and apply, however instead of immediately running the function bind is used when we want to be able to call the function again later, as it returns a new function with a certain context and parameters. This allows for function currying, though that is outside of the scope for this post.
const healWarrior = wizard.heal.bind(warrior, 50);
healWarrior();
console.log('third ', warrior); // health is now 200
So as we can see, when it comes to using the 'this' keyword in javascript there are many pitfalls but once we understand how context and scope work in Javascript, it's not as scary to use.