In short
The JavaScript this keyword depends on how a function is called. Learn the rules for this, fix lost this with bind, call and apply, and see how prototypes work. How is the value of this decided in JavaScript? this is a special word that points to an object.
The JavaScript this keyword depends on how a function is called. Learn the rules for this, fix lost this with bind, call and apply, and see how prototypes work.
How is the value of this decided in JavaScript?
this is a special word that points to an object. The key idea: for normal functions, this is decided when the function is called, not when it is written. It is like the word "here": its meaning depends on where you are when you say it.
Here are the main rules:
| How the function is called | What this is |
|---|---|
As a method: user.greet() | the object before the dot (user) |
As a plain function: greet() | undefined in strict mode and modules (the global window in old sloppy scripts) |
As an event listener: button.addEventListener("click", function () {...}) | the element (button) |
| An arrow function | this from the code around it; arrows have no this of their own |
With new: new User() | the new object |
"use strict";
const user = {
name: "Maya",
greet() {
return `Hi, I'm ${this.name}`;
},
greetArrow: () => `Hi, I'm ${this?.name}`,
};
console.log(user.greet()); // Output: Hi, I'm Maya (method call: this = user)
const greetFn = user.greet;
// greetFn(); // TypeError: Cannot read properties of undefined (reading 'name')
console.log(user.greetArrow()); // Output: Hi, I'm undefined (arrow: this is from outside)
The same function, greet, gives different results depending on how you call it. When you copy it into greetFn and call it alone, there is no object before the dot, so this is undefined. The arrow function ignores the object completely. That is why arrow functions are a bad choice for object methods.
Why do you lose this in callbacks?
The most common this bug happens when you pass a method as a callback. You pass the function, but not the object, so this is lost.
class Timer {
constructor() {
this.seconds = 0;
}
tick() {
this.seconds++;
console.log(`Seconds: ${this.seconds}`);
}
startBroken() {
setTimeout(this.tick, 1000); // passes the function only
}
startFixed() {
setTimeout(() => this.tick(), 1000); // arrow keeps "this"
}
}
const timer = new Timer();
timer.startFixed(); // Output: Seconds: 1 (after 1 second)
timer.startBroken(); // Output: Seconds: NaN (after 1 second: here "this" is window, not the timer)
In startFixed, the arrow function has no this of its own, so it uses the this of startFixed, which is the timer. Wrapping a method in an arrow function is the easiest fix.
Tip: Inside an event listener, use a normal
functionif you wantthisto be the element, or useevent.currentTargetwith an arrow function.event.currentTargetis clearer and works in both.
How to use bind, call and apply
Every function has three methods that let you choose this yourself:
fn.call(obj, a, b)callsfnright now withthis = objand arguments one by one.fn.apply(obj, [a, b])is the same, but the arguments are in an array.fn.bind(obj)does not call it. It returns a new function withthislocked toobjforever.
function introduce(city, country) {
return `${this.name} lives in ${city}, ${country}.`;
}
const maya = { name: "Maya" };
const leo = { name: "Leo" };
console.log(introduce.call(maya, "Lisbon", "Portugal")); // Output: Maya lives in Lisbon, Portugal.
console.log(introduce.apply(leo, ["Osaka", "Japan"])); // Output: Leo lives in Osaka, Japan.
const leoIntro = introduce.bind(leo, "Paris"); // lock this and the first argument
console.log(leoIntro("France")); // Output: Leo lives in Paris, France.
// bind fixes the lost-this problem from the Timer example:
const counter = {
count: 0,
increase() {
this.count++;
console.log(this.count);
},
};
setTimeout(counter.increase.bind(counter), 500); // Output: 1 (after 0.5 seconds)
Today, bind is used mostly for callbacks, and apply is rarer because the spread operator (fn(...args)) does the same job for arguments.
What is the prototype chain?
Every JavaScript object has a hidden link to another object called its prototype. When you read a property that the object does not have, JavaScript looks at the prototype, then the prototype's prototype, and so on. This is the prototype chain. It is like asking a question at a help desk: if the first person does not know, they ask their manager, who may ask their manager.
That is how arrays have push and map: they are not stored on each array, but on Array.prototype, which every array links to.
const animal = {
eats: true,
describe() {
return `${this.name} eats: ${this.eats}`;
},
};
const rabbit = Object.create(animal); // rabbit's prototype is animal
rabbit.name = "Bunny";
console.log(rabbit.describe()); // Output: Bunny eats: true
console.log(Object.hasOwn(rabbit, "eats")); // Output: false (found on the prototype)
console.log(Object.getPrototypeOf(rabbit) === animal); // Output: true
const list = [1, 2, 3];
console.log(Object.getPrototypeOf(list) === Array.prototype); // Output: true
rabbit has only name. The eats property and the describe method are found by walking up the chain to animal. Inside describe, this is still rabbit, because rabbit is the object before the dot.
Classes are built on prototypes
The class syntax from the Classes lesson is a nicer way to write prototypes. Methods you write in a class are stored once, on ClassName.prototype, and all instances share them through the chain.
class User {
constructor(name) {
this.name = name;
}
hello() {
return `Hello, ${this.name}`;
}
}
const u = new User("Ana");
console.log(Object.hasOwn(u, "hello")); // Output: false
console.log(typeof User.prototype.hello); // Output: function
console.log(Object.getPrototypeOf(u) === User.prototype); // Output: true
console.log(typeof User); // Output: function
A class is really a special function, and its methods live on its prototype. extends simply links one prototype to another.
Common mistake: Changing built-in prototypes, like adding
Array.prototype.last = .... It can break other code and future JavaScript features. Write a normal helper function instead.
Practice exercise
Create an object car with a brand property and a method start() that logs "<brand> is starting". Store car.start in a variable and call it, and see that it fails. Then fix it three ways: with bind, with call, and with an arrow function wrapper inside setTimeout.
Hint: const start = car.start.bind(car);.
Key takeaways
- For normal functions,
thisis decided by how the function is called. - Arrow functions do not have their own
this; they use the one around them. - Passing a method as a callback loses
this; fix it with an arrow wrapper orbind. callandapplycall a function with a chosenthis;bindreturns a new bound function.- Objects find missing properties through the prototype chain, and classes are built on it.