Context and Scope in JavaScript, and Other Related Concepts
I intend this article to be part of a series, where I write about fundamental web development topics that are critical for a well-rounded knowledge of front end web development. Although sometimes simple, these are concepts that must be well understood in order to really be proficient as a JavaScript developer.
As a JavaScript developer, can you clearly explain the difference between context and scope? What about hoisting, closures, and the possible different values for “this”?
Two terms in JavaScript development that are often confused with each other are context and scope. They are also part of the fundamentals of the language, that any serious JavaScript developer should seek to understand.
Basically, scope refers to the visibility of variables; it defines in what parts of the application they can be accessed, depending on where they were defined. Context, on the other hand, relates to the object within which a function is called, and is referenced by the “this” keyword. The value that “this” references (a.k.a, the context) changes depending on how a function is called. We’ll cover both topics and some other important related concepts in this article.
Scope
As mentioned before, scope refers to the accessibility of variables from different parts of a JavaScript application. JavaScript has function scope (at least pre-ES6, see more below), meaning that any variables declared inside a function will be available inside that function, but not outside of it. Also any child functions declared inside of the external function will have access to the variables from the parent function.
JavaScript's function scope is in contrast with other programming languages, which have block scope. A block is defined by a pair of curly braces { } , and having block scope allows you to create temporary variables inside structures like if or for loops, which will be inaccessible from outside this block.
This was not the case in JavaScript: When you declare a variable with the “var” keyword, it will be scoped to the whole function where it’s defined, regardless of whether it was declared inside any kind of block structure. If you don’t properly understand this, it can lead to bugs where variables that were meant to be used only inside a block are available outside of it.
A variable declared using var will be available anywhere inside that function, even in lines before the actual declaration (but still inside the same function). This happens because of a feature called hoisting, which means that the variable declaration is moved to the top of the scope (the function where it was declared). Only the declaration is moved, the assignment stays in place.
CAUTION! If you forget to declare a variable using var, it will be declared as a global variable! Javascript will look for the declaration up the function chain, to see if the declaration was in an outer function, finally reaching the global scope and declaring the variable in the global scope.
Starting from the sixth version of ECMAScript (the official specification for the JavaScript language), published in 2015, the language now includes two new keywords to declare variables: let and const, which are block-scoped instead of function-scoped. In general, it is recommended to use let instead of var, since they work the same except for the scope, and having block scope for variables is less error prone.
Const is a bit special, because variables (or should we say, constants) declared with the const keyword cannot be reassigned. If you declare a const and assign it a primitive type (a number, a string, a boolean, null or undefined), this value cannot be changed in any way. If you declare a const that references an object however, it doesn’t cause complete immutability, because the properties of the object can still be changed, but you are prevented from reassigning another object to that constant.
Again, it is recommended to always use const over let and var whenever you can, which is actually in most cases. Using const variables is a good practice because it minimizes mutable state: It minimizes the amount of variables that can change inside of the logic of an application. The more moving parts (for example, the internal state that can be modified) that a piece of software has, the more likely it is to have unexpected bugs. If you declare a variable as a const, you can have the confidence that it will not be changed later on in the program. This is also beneficial in terms of readability and maintainability, because if another programmer or even you have to go back to the code in the future, it will be clear that the intention of that const variable was to remain constant.
Closures
Closures are also a fundamental aspect of the topic of scope in Javascript, that must be understood well. There are a lot of good resources explaining closures, like Eric Elliott’s article “Master the JavaScript Interview: What is a Closure?”. I recommend reading his article for a more detailed explanation. He defines closures as:
“A closure is the combination of a function bundled together (enclosed) with references to its surrounding state (the lexical environment).”
This means that when you have an inner function inside another function, the inner function will have access to the scope of the outer function, including the arguments received by the outer function, and will continue to have access even after the outer function has executed and returned.
Closures in JavaScript are important to implement design patterns such as the module pattern or revealing module pattern, which provide data privacy to objects and functions. This article will not go into details of design patterns, but basically closures make this possible because the inner functions can have access to a scope that will be completely inaccessible to anything else. Closures are also used for functional programming techniques like currying and partial application.
Context
Context in JavaScript doesn’t refer at all to variables and their visibility, context refers to the object within which a function is executed, and it’s referenced by the “this” keyword, which allows you to access its properties or methods. Depending on how a function is called, there are six possible scenarios, where “this” will take different values:
- Outside of any function: If the keyword “this” is used outside of any function, it will refer to the global context. In browsers, it would reference the window object.
- Inside a constructor function: When a function is called as a constructor, using the new keyword, then any usage of “this” inside of the function will refer to the new object instance that will be created. In this way constructors can add properties and methods that will belong to the new instance (not including adding shared methods to the constructor’s prototype, but that is another topic).
- Inside an object method: An object method is a function that is a property on an object. In this case, “this” will be a reference to that object. This is very often used to access and modify other properties or methods of the object.
- Inside a simple function: This means a function that is just declared as a “stand-alone” function, without being a property of any object, and without calling it as a constructor with new. In these cases, “this” will reference the global context (window object in browsers).
- Inside a function that is called as a callback for an event listener: Regardless of where and how a function is defined, if it is called as a callback for an event listener (for example, to be called when a click event is triggered), “this” inside that function will reference the target element of the event (not counting arrow functions). This is a very common situation in JavaScript, and a frequent source of errors, because the “this” value that would probably make more sense to a developer is overridden by the target element of the event. There are several ways to get around this issue, that I’ll mention below.
- Inside an arrow function: In ES6 arrow functions were introduced, and not only do they provide a more concise syntax for writing functions, but they also have the peculiarity that they will retain the same context as its lexical environment (the context of the function's surroundings at the time of its creation), no matter how they are called later. Using arrow functions is actually one of the ways to deal with the issue of event handlers changing the context of callback functions.
Bind, Call, Apply, and other ways to manually specify context
Now that we saw how context is normally determined, it’s also important to mention some techniques that can be used to have more control on the context of a function.
- Arrow Functions: Even though arrow functions can really be considered as one of the six common scenarios that determine context, it is anyways worth noting again that they are a useful technique to use when you want to prevent the context of a function from being changed, such as when calling a function as a callback for an event handler. This way, you know that the function will have the same context from where it was defined.
- Bind: The function bind is available as a method of any JavaScript function (thanks to prototypal inheritance, since bind is a method of Function.prototype). When calling bind on a function, it will return a new version of the function, that will have the context permanently set to whatever object bind receives as its first argument. Bind can also optionally receive more arguments, which will then always be used as arguments for the target function whenever it’s called. This new function can then be safely used as an event handler’s callback, knowing that the context will remain fixed.
- Call: This function is similar to bind, and can also be called on any function. The difference is that call doesn’t return a new version of the target function, it just immediately executes the target function while setting its context to be the object that call() received as an argument. As with bind, call can also receive extra optional parameters, that will be passed as parameters for calling the target function.
- Apply: Apply works exactly the same as call, except that the optional parameters should not be passed individually, but as an array of values. For our purposes, it would be used in the same way as the previous example for call().