JavaScript: Context
Posted by in Programming onJavaScript's eccentric implementation of context is easily one of the language's most misunderstood features — even an expert software engineer can quickly find themselves outside of their comfort zone if they don't already understand the rules. If you are a JavaScript developer and plan to use functions or objects (hint: you totally do), then you're going to need to become comfortable with how JavaScript handles context. Luckily, despite the uniqueness of the rules that govern context in JavaScript, they aren't difficult to master.
What is this?
In JavaScript, this
is a special keyword that represents the context that owns the current scope at runtime. Referencing the this
keyword allows a line of code to access or manipulate that context without needing prior knowledge or explicit access to the value it represents. As a result, programmers are able to write abstract code with reusability.
Some programmers erroneously use the terms "scope" and "context" interchangeably. When used correctly, context refers to the value that "owns" the scope at run-time — represented by the this
keyword.
Consider this metaphor from the article on scope:
Just about everyone's lost their cell phone at one point or another. You reach into your pocket to find your phone, only to realize that it's been misplaced. You quickly check your other pockets, and it's not there either. The next step, you check your immediate surroundings — but it's not there, either. You check the room you are in. Then you check the previous room you were in. You retrace your steps as far back as you need to until you eventually recover your trusty miniature-super-computer.
In this metaphor, your surroundings are the scope — you are the context. While the two concepts are closely related, they are two very distinct ideas that you should keep separate in your mind.
Global Context
The Global Context is referenceed when this
is used outside of a function. It is generally considered to be bad practice to write code directly in the global scope, and so I would avoid against using the Global Context in almost all cases. One notable exception would be the module patterns, which may need to be aware of the global context:
// Using a closure to encapsulate scope
(function (g) {
g.MyModule = {};
})(this);
In the above example, g
would refer to window
in a browser and global
in Node.js. This allows a programmer to write code that could be reused between Node.js and a browser.
Local Context
Whenever a function is executed, a new local context is created to reference the value which executed the function. The processes by which context is determined don't have official names. In the interest of clarity, I will describe them as Implicit Binding, Explicit Binding, and Unresolvable Binding.
Implicit Binding
Implicit Binding is the most commonly used technique, by far. It's flexible and compact, and it's easier to write and understand at-a-glance. However, it also requires the developer to have a stronger understanding how JavaScript will determine which value to use as the context.
function getThis() {
return this;
}
var myObject = { getThis: getThis };
myObject.getThis(); // returns myObject
Implicit Binding is particularly useful because it can be used without closely tying a function's behavior to the object it is assigned to. As a result, a generic function may be created once, and then assigned to an object to achieve the same result:
function getProperty() {
return this.property;
}
var a = {
property: 'Hello',
getProperty: getProperty,
};
var b = {
property: 'World',
getProperty: getProperty,
};
console.log(a.getProperty()); // 'Hello'
console.log(b.getProperty()); // 'World'
A note that may not seem obvious to inexperienced JavaScript developers: the context is implicitly bound to the object a function is executed from. Even if multiple objects are chained together, the context will always reference the object that contains the function:
var myObject = {};
myObject.myProperty = {};
myObject.myProperty.getThis = function() {
return this;
};
myObject.myProperty.getThis(); // returns myObject.myProperty
If you try to cleverly cache the function without the object the function is attached to, then JavaScript will no longer be able to resolve the context properly:
var myObject = {};
myObject.myProperty = {};
myObject.myProperty.getThis = function() {
return this;
};
var func = myObject.myProperty.getThis;
func(); // Does not return myObject or myObject.myProperty
Constructors
Constructors are a special case for implicit bindings. When a function is executed with the new
keyword, the this
value references a freshly created object before that object becomes available to the context that created it. Consider the following example, which sets the name
property before the variable kitty
is assigned:
function Cat(name) {
'use strict';
this.name = name;
}
var kitty = new Cat('Mittens');
kitty.name; // 'Mittens'
In this example, this.name
is available to the Cat
constructor before the object becomes available to the parent scope via the kitty
variable.
Explicit Binding
Explicit Binding is used when a programmer needs to unambiguously declare a function's context to JavaScript. The ability to explicitly declare the context is useful in two circumstances:
- You intend to run a function against an object, but don't know if the function exists on the object.
- You intend to run a function against an object, but you don't want that function to be attached to the object.
In the first case, you may be trying to convert a value from one type to another. Consider the following example, which converts an object into a sparsely populated array:
var array = [].slice.call({ 100: 'Hello', length: 400 });
console.log(array); // [empty x 100, "Hello", empty x 299]
An astute observer probably caught the call
function in the above example. The call
function is one of three standard methods on the Function
prototype that allow a user to declare a function's context to JavaScript: call
, apply
, and bind
.
Call Functions
The call
function accepts all of the same parameters as the function being called, except that it is prefixed with the variable to use as the context. This is useful when you have enough information to call the function directly, but just want to change the context when it executes. One prominent example would be with calling parent constructors:
function Shape(sides) {
this.sides = sides;
}
function Rectangle(height, width) {
Shape.call(this, 4);
this.height = height;
this.width = width;
}
var square = new Rectangle(2, 2);
console.log(square); // { sides: 4, height: 2, width: 2 }
Apply
The apply
method accepts two arguments: a context value and an array of variables to pass into the function. In many ways, it is equivalent to call
, but is especially useful when you are not certain of the contents of the array ahead of time. For example, a function that expands the logic of another function:
var myArray = [];
// Replace the splice function
myArray.splice = function( /* omit arguments */ ) {
console.log('Spliced Again!');
Array.prototype.splice.apply(this, arguments);
return this;
}
Bind
The bind
method is used to prevent a function from having its context changed. This is useful for handling callbacks in libraries that tend to redefine the context of a function (e.g. SharePoint and jQuery). Under the hood, bind
behaves a lot like this:
Function.prototype.bind = function(context) {
// get the function being called
var boundFunction = this;
return function() {
// since "apply" is being run, "this" == "context"
return boundFunction.apply(context, arguments);
};
};
// ECMAScript 6 Version:
Function.prototype.bind = function(context) {
return (...args) => this.apply(context, args);
}
As you can see, bind
works by caching the context
variable, and then returning a wrapper that executes the returned function as a closure. No matter how this function is executed, it can never overwrite the context
variable:
function getThis() {
return this;
}
var myObject = {};
var callback = getMyProperty.bind(myObject);
callback(); // returns myObject
callback.call(null); // returns myObject
callback.apply({}); // returns myObject
Unresolvable Binding
Unresolvable Binding is a special case that can bite a careless JavaScript developer. If a function is executed in the global scope and it isn't attached to an object, then JavaScript doesn't have an obvious value to use as the function's context. Or so it would appear.
If you recall, any variable in the global scope is attached to the global object. If you can access the function from the global scope, then it must be a global variable by definition. So, when a function is executed in the global scope — and the context can't be determined through explicit or implicit binding — then the function is executed against the global context.
If the code is running in strict mode, then this means that this
will actually be undefined
:
function getThis() {
'use strict';
return this;
}
getThis(); // undefined
Otherwise, the global variable will be returned — global
in Node.js, and window
in browsers:
function getThis() {
return this;
}
getThis(); // window
Arrow Functions
No discussion of context would be complete without an honorable mention for Arrow Functions!
Arrow Functions differ from regular functions in that their context is permanently bound to the context in which they were created. That is to say, that an arrow function's context always behaves as if the function had used the bind
method. This has some benefits and drawbacks, but allows developers to use much cleaner syntax when dealing with callbacks:
function saveData(data) {
var that = this;
this.startTime = Date.now();
Database.save(data, function callback(error, result) {
// no idea what "this" is, so we use "that"
that.endTime = Date.now();
});
}
// behaves the same as
function saveData(data) {
this.startTime = Date.now();
Database.save(data, (error, result) => {
// We know exactly what "this" is
this.endTime = Date.now();
});
}
This is a bit of an oversimplification of arrow functions; but for the purposes of a discussion on context, what you need to know about Arrow Functions is that their context cannot be overridden. No matter what you do. The context parameter for bind
, call
, and apply
will be ignored, and this
will always point to the parent scope's context.
Summary
- In JavaScript, the context of a function is the value that owns the scope.
- The scope can be implicitly set by attaching a function to an object.
- The scope can be explicitly set by using the
bind
,call
, orapply
functions. - The scope can be
undefined
orglobal
if JavaScript cannot determine a scope. - Arrow Functions always use the context of the scope they were declared within, and cannot be overridden.