JavaScript's this keyword is a comon source of confusion. Unlike many other languages, the value of this inside a function is not determined by where the function is defined, but by how it is called. In other words, the invocation context decides what this points to.
In JavaScript, a function can be called in several ways: direct call, method call, and constructor call (with new). Additionally, there are special calls using bind(), call(), and apply(). ES6 introduced arrow functions, which have a different behavior regarding this. This article will explore these scenarios.
1. Direct Call
A direct call is simply invoking a function as functionName(...). In this case, this inside the function refers to the global object. In a browser, the global object is window; in Node.js, it is global.
Example:
// Simple compatibility for browser and Node.js global object
const _global = typeof window === "undefined" ? global : window;
function test() {
console.log(this === _global); // true
}
test(); // Direct call
Note that direct call is not limited to the global scope. Calling a function as functionName(...) from any scope is considered a direct call, and this still refers to the global object.
(function(_global) {
// IIFE to limit scope
function test() {
console.log(this === _global); // true
}
test(); // Direct call within a non-global scope
})(typeof window === "undefined" ? global : window);
Impact of bind() on Direct Calls
The Function.prototype.bind() method creates a new function with a fixed this value. Regardless of how the bound function is called, this will always point to the bound object.
const obj = {};
function test() {
console.log(this === obj);
}
const testObj = test.bind(obj);
test(); // false
testObj(); // true
To illustrate how bind() works, consider this simple simulation:
function myBind(func, target) {
return function() {
return func.apply(target, arguments);
};
}
const testObj = myBind(test, obj);
test(); // false
testObj(); // true
The simulation uses a closure to retain target and then applies func using apply(). While the native bind() implementation is more efficient, this example shows its core logic.
Impact of call() and apply()
Function.prototype.call() and Function.prototype.apply() also allow you to set the this value for a function call. Their first argument specifies the this context.
However, if the function is already bound using bind(), call() and apply() will not override the bound this.
const obj = {};
function test() {
console.log(this === obj);
}
const testObj = test.bind({});
test.apply(obj); // true
testObj.apply(obj); // false
2. Method Call
A method call occurs when a function is called as a property of an object, using the syntax object.method(...). In this case, this refers to the object that owns the method. However, bind() can also affect this behavior.
const obj = {
test() {
console.log(this === obj);
}
};
obj.test2 = function() {
console.log(this === obj);
};
function t() {
console.log(this === obj);
}
obj.test3 = t;
obj.test4 = (function() {
console.log(this === obj);
}).bind({});
obj.test(); // true
obj.test2(); // true
obj.test3(); // true
obj.test4(); // false
Even if a function is defined separately and then assigned to an object, this is determined by the call pattern, not the definition.
When this Points to the Global Object in a Method
This happens when a method is called directly instead of as a method. For example:
const obj = {
test() {
console.log(this === obj);
}
};
const t = obj.test;
t(); // false
This is a common pitfall when passing object methods as callbacks. Consider this example with jQuery:
class Handlers {
constructor(data, $button) {
this.data = data;
$button.on("click", this.onButtonClick);
}
onButtonClick(e) {
console.log(this.data);
}
}
const handlers = new Handlers("string data", $("#someButton"));
// Clicking the button outputs undefined, not "string data"
The issue is that this.onButtonClick is passed as a callback and later invoked as a direct call, losing its original context. To fix this, you can:
// Solution 1: Store a reference to this
var _this = this;
$button.on("click", function() {
_this.onButtonClick();
});
// Solution 2: Use bind()
$button.on("click", this.onButtonClick.bind(this));
// Solution 3: Use an arrow function (caution with jQuery)
$button.on("click", e => this.onButtonClick(e));
Be careful when using arrow functions with jQuery, as this inside the callback will not be the target element.
3. Constructor Call with new
Before ES6, any function could be used as a constructor with the new keyword. When a function is called with new, a new object is created, and this points to that new object.
var data = "Hi"; // Global variable
function AClass(data) {
this.data = data;
}
var a = new AClass("Hello World");
console.log(a.data); // Hello World
console.log(data); // Hi
var b = new AClass("Hello World");
console.log(a === b); // false
ES6 introduced classes, but they still behave similarly. However, methods defined inside a class cannot be called with new.
4. Arrow Functions and this
Arrow functions do not have their own this binding. The value of this inside an arrow function is inherited from the enclosing non-arrow function or context.
const obj = {
test() {
const arrow = () => {
// `this` is taken from test()'s context
console.log(this === obj);
};
arrow();
},
getArrow() {
return () => {
// `this` is taken from getArrow()'s context
console.log(this === obj);
};
}
};
obj.test(); // true
const arrow = obj.getArrow();
arrow(); // true
Arrow functions eliminate the need for var _this = this closures. Babel's transpilation of the above example illustrates this:
// ES6
const obj = {
getArrow() {
return () => {
console.log(this === obj);
};
}
};
// Transpiled to ES5
var obj = {
getArrow: function getArrow() {
var _this = this;
return function () {
console.log(_this === obj);
};
}
};
Arrow functions cannot be used as constructors and are not affected by bind(), call(), or apply() to change their this.
Original article by 边城 on SegmentFault