JavaScript’s this Keyword, Four Rules and One Exception
this depends on how a function is called, not where it is written. Four call-site rules, the arrow function exception and how to fix a lost this in callbacks.
- 9 分で読めます
このページはまだ日本語に対応していないため、英語の原文を表示しています。
In JavaScript, this is decided at the call site: the same function gets a different this depending on how it is called. Four rules cover every regular function, checked in a fixed order, and arrow functions are the one exception because they have no this of their own. This post covers the rules, a decision tree for debugging them, and the three places this usually gets lost.
Why It Still Matters
With function components and hooks, many React developers rarely write this any more. It hasn’t gone away. You still meet it in:
- class-based SDKs and API clients whose methods get passed around as callbacks
- DOM event listeners
- Web Components (every custom element is a class)
- Node.js event emitters
- most frontend interviews
When it breaks, the error shows up far from the cause: TypeError: Cannot read properties of undefined (reading 'queue'), thrown inside a timer or an array method, with nothing in the stack trace pointing at the line that dropped this. this is one part of what JavaScript sets up for every call; execution contexts and closures covers the rest.
The Four Rules, in Order
Ask these questions about the call, top to bottom. The first one that matches decides this.
| Rule | The call looks like | this is |
|---|---|---|
1. new | new Tracker() | the newly created object |
| 2. Explicit | fn.call(obj), fn.apply(obj), bound() | obj, or the value given to bind |
| 3. Method | obj.fn() | obj, the object before the dot |
| 4. Plain | fn() | undefined in strict mode, else globalThis |
1. Called With new
new does three things, and class constructors work the same way:
- creates an empty object whose prototype is the constructor’s
prototype - calls the function with
thisset to that object - returns the object
function Tracker(name) {
this.name = name;
this.queue = [];
}
const checkout = new Tracker('checkout');
checkout.name; // 'checkout'new even beats bind. Calling a bound function with new ignores the bound this (the bound arguments still apply):
const Bound = Tracker.bind({ name: 'ignored' });
new Bound('search').name; // 'search'2. Called With call, apply or bind
call and apply run the function once with the this you pass. bind returns a new function with this fixed for good: calling it with call, or binding it again, does not change it.
function describe(prefix) {
return `${prefix} ${this.name}`;
}
const user = { name: 'Ada' };
describe.call(user, 'Hi'); // 'Hi Ada'
describe.apply(user, ['Hi']); // 'Hi Ada'
const greet = describe.bind(user);
greet('Hello'); // 'Hello Ada'
greet.call({ name: 'Bob' }, 'Hey'); // 'Hey Ada': a bound function keeps its thisIn sloppy (non-strict) code, passing null or undefined makes this the global object, and primitives such as 42 are wrapped in objects. Strict code passes the value through unchanged.
3. Called as a Method
When the call is obj.fn(), this is the object before the dot at the moment of the call. It is not the object the function was written in:
const counter = {
count: 0,
increment() {
this.count += 1;
return this.count;
},
};
counter.increment(); // 1
const other = { count: 10, increment: counter.increment };
other.increment(); // 11: same function, different object before the dotThis is also why class methods work: they live on the prototype, but you call them as instance.method(), so this is the instance. Optional chaining (counter?.increment()) and brackets ((counter.increment)()) still count as method calls.
4. A Plain Call
Nothing matched, so it is a plain call: fn(). In strict mode this is undefined. ES modules and class bodies are always strict, so in modern code this is what you get:
function whoAmI() {
return this;
}
whoAmI(); // undefined in a module or class; globalThis in a sloppy scriptIn an old-style script without 'use strict', this becomes globalThis (window in browsers), so this.count = 1 silently creates a global variable. The strict-mode TypeError is the better failure: it is loud and it happens at the right place.
One Function, Four Calls
The rules are easiest to remember as one picture: the same function, called four ways, gets four different values of this. Nothing about the function changed; only the call did.
The Exception: Arrow Functions
An arrow function has no this of its own. It uses the this of the code it was written in, and call, apply and bind can’t change that. Arrow functions also can’t be called with new (TypeError: arrow is not a constructor).
That makes them the right tool for callbacks inside methods:
class Tracker {
queue: string[] = [];
start() {
// The arrow function uses start()’s this: the instance.
setInterval(() => this.flush(), 3000);
}
flush() {
// send this.queue …
}
}And the wrong tool for object methods:
// In an ES module, where top-level this is undefined
const user = {
name: 'Ada',
greet: () => this.name, // this is the module’s this, not user
};
user.greet(); // TypeError: Cannot read properties of undefined (reading 'name')Is That Everything? this Outside a Function Call
The four rules and the arrow exception cover every call of a function. this also has a value in a few places that aren’t function calls:
| Where | this is |
|---|---|
| Top level of an ES module | undefined |
| Top level of a classic browser script | The global object (window) |
| Top level of a CommonJS module (Node.js) | module.exports |
A class field initializer (count = this.start) | The instance being created |
A static { … } block, or a static field initializer | The class itself |
Direct eval('this') | The this of the code around it |
So the full answer is: the call decides this for regular functions, the surrounding code decides it for arrow functions, and the table above covers code that runs outside any call. When you need the global object, use globalThis rather than a top-level this.
Where This Gets Lost
Passing a Method as a Callback
This is the most common bug, and it follows directly from rules 3 and 4:
class Tracker {
queue = [];
track(event) {
this.queue.push(event);
}
}
const tracker = new Tracker();
['click', 'scroll'].forEach(tracker.track);
// TypeError: Cannot read properties of undefined (reading 'queue')tracker.trackis read as a value: the function alone, withouttrackerforEachkeeps it as a plain callbackforEachcalls it with no object before the dot- The class body is strict, so
thisisundefined
Any of these fixes it:
['click', 'scroll'].forEach((event) => tracker.track(event)); // a method call again
['click', 'scroll'].forEach(tracker.track, tracker); // array methods accept a thisArg
['click', 'scroll'].forEach(tracker.track.bind(tracker)); // fixed for goodTimers have the same problem: setTimeout(tracker.flush, 1000) calls flush as a plain function.
Event Listeners
A regular function passed to addEventListener is called with this set to the element the listener is attached to. An arrow function keeps the surrounding this instead. Reading event.currentTarget is clearer than either:
button.addEventListener('click', function (event) {
this === event.currentTarget; // true: the button
});Destructuring and Indirect Calls
Taking a method off its object loses this too, however you do it:
const { track } = tracker;
track('click'); // TypeError: no object before the dot
(0, tracker.track)('click'); // TypeError: the comma operator returns the bare functionBundlers emit (0, fn)() on purpose, to call imported functions without a this.
Where You Meet It Day to Day
The same rules, in the places they usually bite.
A Handler in a React Class Component
Class components are still common in older codebases and error boundaries. React calls your handler as a plain function, so a regular method loses the instance:
class SaveButton extends React.Component {
handleClick() {
this.props.onSave(); // TypeError: this is undefined
}
render() {
return <button onClick={this.handleClick}>Save</button>;
}
}The usual fix is an arrow class field, handleClick = () => { this.props.onSave(); };, so this is fixed to the instance when the object is created.
A Method Passed to then
Service classes often hand a method to a promise chain. then calls it as a plain function:
class UserService {
#cache = new Map();
remember(user) {
this.#cache.set(user.id, user);
return user;
}
load(id) {
return fetch(`/api/users/${id}`)
.then((response) => response.json())
.then(this.remember); // TypeError when the response arrives
}
}The error appears later, inside a promise, with nothing in the stack trace pointing at load. Write .then((user) => this.remember(user)).
Route Handlers in Node.js
The same thing happens when a framework stores your method and calls it later:
class OrdersController {
constructor(repository) {
this.repository = repository;
}
async list(request, response) {
response.json(await this.repository.all());
}
}
const orders = new OrdersController(repository);
app.get('/orders', orders.list); // TypeError on every request
app.get('/orders', (request, response) => orders.list(request, response)); // worksArrow Functions in an Options Object
Frameworks with an “options object” style, such as Vue’s Options API, call your methods with this set to the component. An arrow function can’t receive that this:
export default {
data() {
return { count: 0 };
},
methods: {
increment() {
this.count++; // this is the component
},
reset: () => {
this.count = 0; // this is the module’s this (undefined), not the component
},
},
};Event Emitters in Node.js
Node.js calls a regular listener function with this set to the emitter, and an arrow function keeps its surrounding this:
import { EventEmitter } from 'node:events';
const emitter = new EventEmitter();
emitter.on('ready', function () {
console.log(this === emitter); // true
});
emitter.on('ready', () => {
console.log(this === emitter); // false: the module’s this
});
emitter.emit('ready');As with DOM listeners, reading the object you need from a variable or the event is clearer than relying on this.
Going Deeper: Classes, Getters and Types
Derived Constructors Must Call super() First
In a class that extends another, this doesn’t exist until the parent constructor has created it:
class Base {}
class Child extends Base {
constructor() {
this.x = 1; // too early
super();
}
}
new Child();
// ReferenceError: Must call super constructor in derived class before accessing 'this'The parent class builds the object, so the child can only touch it after super() returns. Class fields declared in the child are added at that moment too, which is why they can’t be read inside the parent’s constructor.
Static Methods and Getters Follow the Same Rules
- In a static method,
thisis whatever is before the dot, usually the class itself. Called through a subclass,Sub.create(), it is the subclass, which lets static factory methods create the right type. Passed around as a callback, it losesthislike any other method. - A getter or setter runs with
thisset to the object the property was read from, even when the getter is inherited. Ifadmininherits anuppergetter fromuser,admin.upperusesadmin’s data.
Let TypeScript Check this for You
TypeScript can declare what this must be with a fake first parameter, and it reports calls that would lose it:
function onClick(this: HTMLButtonElement, event: MouseEvent) {
this.disabled = true;
}
button.addEventListener('click', onClick); // fine: the listener’s this is the button
onClick(new MouseEvent('click'));
// error TS2684: The 'this' context of type 'void' is not assignable to method's 'this'With noImplicitThis (part of strict), TypeScript also flags any use of this whose type it can’t work out, which catches most “lost this” bugs before they reach a browser.
Which Fix to Choose
| Fix | Use it when | What it costs |
|---|---|---|
| Arrow function at the call site | A one-off callback | Nothing worth measuring; the intent is visible where the call happens |
bind once and keep the reference | You need to remove the listener later | A little setup code |
Arrow class field (track = (e) => {…}) | A public API whose methods are passed around | One copy per instance instead of one on the prototype; subclasses can’t call super.track() |
thisArg | Array methods such as forEach, map, filter | Only some APIs accept one, and readers often miss it |
No this at all (closures and factories) | Small modules where you control the design | A different style; no shared prototype |
Test Yourself
Each snippet runs in an ES module (strict mode). Predict the output, then open the answer. Every answer was checked by running the code.
チャレンジ 1
The Same Function, Two Calls
const user = {
name: 'Ada',
greet() {
return `Hi ${this?.name}`;
},
};
const greet = user.greet;
console.log(user.greet(), '|', greet());答えを見る答えを隠す
Hi Ada | Hi undefined
user.greet()is a method call, sothisisuser.greet()is a plain call, sothisisundefinedandthis?.nameisundefined.- Without the
?., the second call would throw.
チャレンジ 2
Function vs Arrow in a Timer
const counter = {
start() {
setTimeout(function () {
console.log('function:', this === counter);
}, 0);
setTimeout(() => {
console.log('arrow:', this === counter);
}, 0);
},
};
counter.start();答えを見る答えを隠す
function: false, then arrow: true
- The timer calls the regular function without
counter(browsers passwindow, Node.js passes itsTimeoutobject). - The arrow function uses the
thisofstart(), which was called ascounter.start().
チャレンジ 3
Can You Rebind a Bound Function?
function label() {
return this.name;
}
const a = { name: 'a', label };
const b = { name: 'b', label: label.bind({ name: 'bound' }) };
console.log(a.label(), b.label(), b.label.call({ name: 'call' }));答えを見る答えを隠す
a bound bound
bindfixesthisfor good: neither a method call (b.label()) norcallcan change it.- Only
newoverrides a boundthis.
チャレンジ 4
The Callback Inside a Method
const cart = {
items: [5, 10],
total: 0,
sum() {
this.items.forEach(function (price) {
this.total += price;
});
return this.total;
},
};
console.log(cart.sum());答えを見る答えを隠す
TypeError: Cannot read properties of undefined (reading 'total')
this.itemsworks, becausesumis called as a method.forEachcalls the inner regular function as a plain call, so itsthisisundefined.- Fix: an arrow function,
(price) => { this.total += price; }, which prints15.
チャレンジ 5
An Arrow Class Field Passed Around
class Stopwatch {
seconds = 0;
tick = () => {
this.seconds += 1;
};
}
const watch = new Stopwatch();
const { tick } = watch;
tick();
tick();
console.log(watch.seconds);答えを見る答えを隠す
2
- An arrow class field is created per instance and captures that instance as
this. - So destructuring it off the object doesn’t lose anything.
チャレンジ 6
A Function in an Array
const callbacks = [
function () {
return this.length;
},
];
console.log(callbacks[0]());答えを見る答えを隠す
1
callbacks[0]()is a method call on the array: bracket access counts, just like a dot.- So
thisis the array, and itslengthis 1.
Key Takeaways
thisis set by how a function is called, not by where it is written.- Check the call in order:
new, thencall/apply/bind, thenobj.method(), then a plain call. - Arrow functions have no
this. They use the surrounding one, and nothing can rebind it. - Passing
obj.methodpasses the function withoutobj. Wrap it, bind it or make it an arrow field. - In modules and classes a plain call gives
undefined, which fails loudly and early. That is a feature.
Methods find their functions through the prototype chain, which is covered in what happens when you read a property. Practise the rules in the JavaScript round of “What does this print?”.
Sources
- MDN: super
- TypeScript handbook: Declaring this in a function
- MDN: this
- MDN: Arrow function expressions
- MDN: Function.prototype.bind()
- MDN: The value of this within the handler
- ECMAScript specification: OrdinaryCallBindThis
タグ
よくある質問
Why is this undefined inside my callback?
Passing obj.method hands over the function alone, without obj. The code that receives it calls it as a plain function, and in strict mode (every ES module and class body) a plain call gives undefined. Wrap it in an arrow function, bind it, or define the method as an arrow class field.
Should I use arrow functions everywhere?
NoArrow functions are the right choice for callbacks inside methods, because they keep the method’s this. They are the wrong choice for object methods that need the object, for constructors (an arrow function can’t be called with new) and for event handlers that rely on this being the element.
What is this at the top level of a file?
In an ES module it is undefined. In a classic browser script it is the global object, window. In a CommonJS module in Node.js it is module.exports. If you need the global object, use globalThis, which works everywhere.
What does bind return?
A new function with this (and optionally some leading arguments) fixed permanently. The original function is unchanged, and each call to bind creates a different function, which matters when removing event listeners.
Why can’t I use this before super() in a subclass constructor?
In a derived class the parent constructor creates the object. Until super() returns there is no this to use, so JavaScript throws a ReferenceError if you touch it.
How can TypeScript help with this?
Declare the expected type as a first this parameter, for example function handler(this: HTMLButtonElement), and enable strict mode (noImplicitThis). TypeScript then reports calls that would lose or mismatch this.
Why don’t React function components use this?
They are plain functions called by React, and their state lives in hooks rather than on an instance. Data flows through props, state and closures, so there is no this to lose.
What is globalThis?
A standard name for the global object in every environment: window in browsers, global in Node.js, self in workers. Use it instead of relying on this at the top level, which differs between scripts and modules.