JavaScript (ES2026) Module 5 – Objects in depth
Methods and the this keyword
Predict what this is in method calls, plain calls, constructors and arrows, fix methods that lose this as callbacks, and see why modules give undefined.
What you will learn
- Predict this for method calls, plain calls, constructor calls and arrow functions
- Fix lost this when methods are passed as callbacks
- Explain why this is undefined in module code
Before you start
What you’ll learn · Before you start
What you will learn
- Predict this for method calls, plain calls, constructor calls and arrow functions
- Fix lost this when methods are passed as callbacks
- Explain why this is undefined in module code
Before you start
On this page
Inside a regular function, this is a value that each call supplies, like a hidden extra argument. It is not
fixed where the function is written, so the only reliable way to predict it is to look at the call. A method
call, cart.add(item), makes this the object before the dot, cart. A plain call, add(item), has nothing
before a dot, so this is undefined in strict code (every module and class is strict) and globalThis in old
sloppy scripts. A constructor call with new makes this the object being built. An arrow function has no
this of its own and uses the this of the code around it. The usual bug follows from the first two rules: pass
cart.add to forEach, a timer or a promise, and it is later called without cart, so this is lost. This lesson
runs one method seven ways, states the rules in the order they apply, explains this outside functions and shows
four fixes for lost this.
The call decides
// One method, called seven ways. What this is depends only on how the call is written.
const counter = {
whoAmI() {
if (this === undefined) return 'undefined';
if (this === counter) return 'counter';
return Array.isArray(this) ? 'an array' : 'another object';
},
};
const calls = {
'counter.whoAmI()': () => counter.whoAmI(),
"counter['whoAmI']()": () => counter['whoAmI'](),
'counter.whoAmI?.()': () => counter.whoAmI?.(),
'(counter.whoAmI)()': () => (counter.whoAmI)(),
'const f = counter.whoAmI; f()': () => {
const f = counter.whoAmI;
return f();
},
'(0, counter.whoAmI)()': () => (0, counter.whoAmI)(),
'[counter.whoAmI][0]()': () => [counter.whoAmI][0](),
};
for (const [call, run] of Object.entries(calls)) console.log(call.padEnd(31), 'this is', run()); Output
counter.whoAmI() this is counter counter['whoAmI']() this is counter counter.whoAmI?.() this is counter (counter.whoAmI)() this is counter const f = counter.whoAmI; f() this is undefined (0, counter.whoAmI)() this is undefined [counter.whoAmI][0]() this is an array
Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node lost-this.mjs
Runs on this device, in your browser. The first run downloads JavaScript (about 0.6 MB), which is kept for the next runs.
Your run, in this browser
The same function gives three answers. What matters is whether the expression just before the parentheses is still a property access at the moment of the call:
counter.whoAmI(),counter['whoAmI']()andcounter.whoAmI?.()all call a property ofcounter, sothisiscounter. Optional chaining keeps the object too.(counter.whoAmI)()is the same call: the specification says plain parentheses return what is inside them untouched, property access included.const f = counter.whoAmI; f()stores the function in a variable first. The callf()has nothing before a dot, so it is a plain call andthisisundefined. The function does not remember where it came from.(0, counter.whoAmI)()uses the comma operator, which evaluates both sides and returns the plain value of the right one. The property access is gone, sothisisundefined. Build tools often write calls to imported functions in exactly this shape, because such a call must not pass athis.[counter.whoAmI][0]()is a method call too, on the array: the object before the brackets is the array, sothisis the array.
In the specification’s terms, a call whose function comes from a property reference passes the reference’s base
object as this; any other call passes undefined. A regular function in strict code keeps undefined, while a
sloppy function replaces undefined or null with globalThis and wraps a primitive value in an object.
The rules, in the order they apply
// The rules for this, one at a time. Module code is strict, so a plain call gets undefined.
function plain() {
return this;
}
console.log('plain call:', plain());
function Ticket(seat) {
this.seat = seat; // with new, this is the object being created
}
const ticket = new Ticket('B2');
console.log('new Ticket:', ticket.seat, ticket instanceof Ticket);
// A method found on the prototype still gets the object before the dot.
const user = {
first: 'Asha',
last: 'Rao',
greet() {
return `Hello, ${this.first}`;
},
get full() {
return `${this.first} ${this.last}`;
},
};
const admin = Object.create(user); // admin inherits greet and full from user
admin.first = 'Ravi';
console.log(admin.greet(), '|', admin.full);
// Inside a method: an arrow keeps the method's this, a nested regular function does not.
const timer = {
label: 'T1',
start() {
const viaArrow = () => this.label;
function viaFunction() {
return this;
}
return [viaArrow(), viaFunction()];
},
};
console.log(timer.start()); Output
plain call: undefined new Ticket: B2 true Hello, Ravi | Ravi Rao [ 'T1', undefined ]
Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node four-rules.mjs
Runs on this device, in your browser. The first run downloads JavaScript (about 0.6 MB), which is kept for the next runs.
Your run, in this browser
How a call decides what this is, asked in order
Text description of the diagram
The diagram is a chain of questions asked in order; the first "yes" decides `this`.
- Is the function an arrow function? Then `this` is the `this` of the code around the arrow, whatever the call looks like.
- Is it called with `new`? Then `this` is the new object being created.
- Is it a function made by `bind()`, or is it called through `call()` or `apply()`? Then `this` is the value that was given to them.
- Is it called as a method, `obj.method()`? Then `this` is `obj`, the object before the dot.
- Otherwise it is a plain call, `f()`. Then `this` is `undefined` in strict code, which includes modules and classes, and `globalThis` in sloppy scripts.
- Arrow function:
thiscomes from the code around it, whatever the call looks like.viaArrowinsidestart()returns the timer’s label because it usesstart’sthis. new:new Ticket('B2')creates an object and runsTicketwiththisset to it.- Explicit: a function made by
bind(), or called throughcall()orapply(), gets thethisit was given. The next lesson covers these three. - Method call:
obj.method()getsobj. It is the object before the dot, not the object where the method was found:admin.greet()runs a method that lives onuser, the prototype, yetthisisadmin, so it greets Ravi. Getters follow the same rule, which is whyadmin.fullreadsRavi Rao. - Plain call:
undefinedin strict code,globalThisin sloppy code.viaFunction()insidestart()is a plain call, so it getsundefinedeven though it is written inside a method. Being nested says nothing aboutthis; only the call does.
Methods written with the short syntax, greet() { … }, and methods written as greet: function () { … } follow these
rules in the same way. The short syntax cannot be called with new and can use super, but its this is the same.
this outside any function
The top level of a file has a this too, and it depends on the kind of file:
// What this is outside any function depends on the kind of file. node:vm runs a string as a classic script, the way
// a browser runs a <script> tag without type="module".
import { runInThisContext } from 'node:vm';
console.log('ES module, top level:', this);
console.log('classic script, top level is globalThis:', runInThisContext('this === globalThis'));
console.log('classic script, plain call of a sloppy function gets globalThis:', runInThisContext('(function () { return this === globalThis; })()'));
console.log('classic script, plain call of a strict function gets:', runInThisContext('(function () { "use strict"; return this; })()')); Output
ES module, top level: undefined classic script, top level is globalThis: true classic script, plain call of a sloppy function gets globalThis: true classic script, plain call of a strict function gets: undefined
Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node top-level-this.mjs
// A CommonJS file: Node.js wraps it in a function and calls that function with module.exports as this.
console.log('CommonJS, top level this === module.exports:', this === module.exports);
exports.version = '1.0';
console.log('so this.version is', this.version);
// CommonJS code is sloppy unless it says 'use strict', so a plain call gets globalThis.
function plain() {
return this === globalThis;
}
console.log('plain call gets globalThis:', plain()); Output
CommonJS, top level this === module.exports: true so this.version is 1.0 plain call gets globalThis: true
Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node top-level-this.cjs
- In an ES module, top-level
thisisundefined. The specification defines it that way for module code, and modules are strict, so plain calls inside them getundefinedas well. - In a classic script, a browser
<script>tag withouttype="module", top-levelthisisglobalThis, the window. Sloppy functions in that script getglobalThisfrom plain calls; strict ones getundefined. - In a CommonJS file, Node.js wraps the file’s code in a function before running it and calls that function with
module.exportsasthis, so top-levelthisis the exports object.
This is why old code that relied on this meaning the global object breaks when it is moved into a module. A pattern
such as (function (root) { root.track = … })(this) passes undefined as root in a module and throws when it sets
a property. globalThis names the global object in every kind of file and every modern host; use it whenever you
really mean the global object.
Methods passed as callbacks
When you pass cart.add somewhere, you pass the function and nothing else. Whoever calls it later decides this,
and forEach, promise handlers and most library callbacks call it as a plain function:
// A method passed as a callback is called without its object, so it loses its this. Four fixes, tried in turn.
const cart = {
items: [],
add(item) {
this.items.push(item);
},
};
function attempt(label, run) {
try {
run();
console.log(`${label.padEnd(30)} ok, items: ${cart.items.join(', ')}`);
} catch (error) {
console.log(`${label.padEnd(30)} ${error.name}`);
}
}
attempt('forEach(cart.add)', () => ['pen'].forEach(cart.add));
attempt('an arrow that calls the method', () => ['pen'].forEach((item) => cart.add(item)));
attempt('forEach(cart.add, cart)', () => ['ink'].forEach(cart.add, cart));
const addToCart = cart.add.bind(cart); // one bound function, made once
attempt('forEach(addToCart)', () => ['nib'].forEach(addToCart));
// Many APIs call their handler as a plain function, like this one.
function onScan(handler) {
handler('ruler');
}
attempt('onScan(cart.add)', () => onScan(cart.add));
attempt('onScan(addToCart)', () => onScan(addToCart));
// Promises call their handlers without a this too.
await Promise.resolve('glue')
.then(cart.add)
.then(
() => console.log('then(cart.add) worked'),
(error) => console.log(`${'then(cart.add)'.padEnd(30)} ${error.name}`),
);
await Promise.resolve('glue').then(addToCart);
console.log('items at the end:', cart.items); Output
forEach(cart.add) TypeError an arrow that calls the method ok, items: pen forEach(cart.add, cart) ok, items: pen, ink forEach(addToCart) ok, items: pen, ink, nib onScan(cart.add) TypeError onScan(addToCart) ok, items: pen, ink, nib, ruler then(cart.add) TypeError items at the end: [ 'pen', 'ink', 'nib', 'ruler', 'glue' ]
Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node callbacks.mjs
Runs on this device, in your browser. The first run downloads JavaScript (about 0.6 MB), which is kept for the next runs.
Your run, in this browser
forEach(cart.add), onScan(cart.add) and then(cart.add) all fail with a TypeError: add runs with this set
to undefined and cannot read this.items. The array method calls its callback with the thisArg you pass it,
undefined by default, and a promise calls its handlers with undefined too. Timers differ by host: Node.js passes
its Timeout object as this and browsers pass the window, so a method there may write to the wrong object without
any error, which is harder to notice than a TypeError.
The fixes:
- An arrow at the call site,
items.forEach((item) => cart.add(item)), calls the method withcartbefore the dot. It is the clearest fix when you write the call yourself, and it leaves the method an ordinary method. - The
thisArgparameter offorEach,map,filter,some,everyandfind:forEach(cart.add, cart). Only array methods and a few others offer it. - A bound function,
const addToCart = cart.add.bind(cart), made once and passed everywhere. Because it is one function, you can also remove it again, for example withremoveEventListener, which an arrow written inline at each call site does not allow. - No
thisat all: build the object in a function and let the methods reach it through a variable the function holds (a closure), or bind every method in the constructor. Then a method works however it is called, which the exercise below asks for.
| Criterion | Arrow at the call site | thisArg | bind once | Bound or closure methods |
|---|---|---|---|---|
| Where the fix lives | Each caller | Each caller | One shared function | The object itself |
| Works with any API | Yes | Only APIs with a thisArg | Yes | Yes |
| Same function every time, so it can be removed | No | Yes | Yes | Yes |
| Cost | A small function per call site | None | One bound function | One function per method per object |
| When to choose | Calls you write yourself | Array methods | Event listeners you must remove later | Objects whose methods are handed out everywhere |
Arrow functions are the right tool inside methods, for callbacks that need the method’s this, and the wrong
tool as methods of an object literal, where they would take this from the surrounding module, which is
undefined. The arrow functions lesson shows both cases.
Exercise: a cart whose methods survive being passed around
Exercise · Medium · JavaScript
A cart whose methods survive being passed around
createCart() in cart.mjs returns a cart with five methods. They work when you call them on the cart, as in cart.add('pen', 2), but the rest of the shop passes them around: const { add } = cart, a barcode scanner that calls handler(sku), and setTimeout inside clearLater(). Each of those calls loses the cart as this.
Change createCart() so that every method works however it is called:
add(sku, qty = 1)adds{ sku, qty }toitemsand returns the cart, so calls can be chained.remove(sku)removes every item with thatskuand returns the cart.total(prices)returns the sum ofprices[sku] * qtyover the items.clear()emptiesitems;clearLater(ms)callsclear()aftermsmilliseconds.- Two carts made by two calls of
createCart()never share items.
Keep items a property of the cart: the tests read cart.items.
Starter code · cart.mjs
// createCart(): a cart whose methods should keep working when they are passed around without the cart
// (the exercise of lesson javascript/methods-and-this). Today they only work as cart.method(...).
export function createCart() {
return {
items: [],
add(sku, qty = 1) {
this.items.push({ sku, qty });
return this;
},
remove(sku) {
this.items = this.items.filter((item) => item.sku !== sku);
return this;
},
total(prices) {
return this.items.reduce((sum, item) => sum + prices[item.sku] * item.qty, 0);
},
clear() {
this.items = [];
},
clearLater(ms) {
setTimeout(this.clear, ms);
},
};
} The sample tests · cart.test.mjs
import { test, assert } from 'mysmartcopilot:test';
import { createCart } from './cart.mjs';
const prices = { pen: 1500, ink: 4000, nib: 250 };
test('the methods work when called on the cart, and add chains', () => {
const cart = createCart();
assert.equal(cart.add('pen', 2).add('ink'), cart);
assert.equal(cart.total(prices), 7000);
assert.equal(cart.remove('pen'), cart);
assert.deepEqual(cart.items, [{ sku: 'ink', qty: 1 }]);
});
test('add and remove work after being taken off the cart', () => {
const cart = createCart();
const { add, remove } = cart;
add('pen');
add('ink', 3);
remove('pen');
assert.deepEqual(cart.items, [{ sku: 'ink', qty: 3 }]);
});
test('add works as a handler that is called as a plain function', () => {
const cart = createCart();
const handlers = [];
const scanner = {
onScan: (handler) => handlers.push(handler),
scan: (sku) => handlers.forEach((handler) => handler(sku)),
};
scanner.onScan(cart.add);
scanner.scan('nib');
scanner.scan('nib');
assert.deepEqual(cart.items, [
{ sku: 'nib', qty: 1 },
{ sku: 'nib', qty: 1 },
]);
});
test('total works after being taken off the cart', () => {
const cart = createCart().add('nib', 4).add('ink');
const { total } = cart;
assert.equal(total(prices), 5000);
});
test('clearLater empties the cart once the time has passed', async () => {
const cart = createCart().add('pen');
cart.clearLater(50);
assert.equal(cart.items.length, 1, 'the cart should not be empty yet');
await new Promise((resolve) => setTimeout(resolve, 60));
assert.deepEqual(cart.items, []);
});
test('two carts never share items', () => {
const first = createCart();
const second = createCart();
first.add('pen');
assert.deepEqual(second.items, []);
assert.notEqual(first.add, second.add);
}); A hint
A method called as handler(sku) gets undefined as this, because nothing stands before a dot, and a timer does not pass the cart either. Two ways out: after building the object, replace each method with a bound copy, cart.add = cart.add.bind(cart); or stop using this and refer to the object through the variable that holds it, cart.items.push(…), since every method is created inside createCart() and can see that variable.
Results of the sample tests
| Test | Result | Details |
|---|
What your code printed
The sample tests run on this device, in your browser (QuickJS): nothing is sent to mysmartcopilot.com. The first run downloads JavaScript (about 0.6 MB), which is kept for the next runs. A check in your browser is feedback for you, not proof that the code is right for every input.
Self-check
Check yourself
6 questions about this lesson. Every answer and why it is right is on the page, behind “Show the answer”. Your score stays in this browser.
Interview questions
Fresher · How does JavaScript decide what this is inside a regular function?
By the way the function is called, not where it is written. A method call such as cart.add(item) makes this the
object before the dot, cart, even when the method was found on a prototype. A plain call such as add(item) gives
undefined in strict code, which includes modules and classes, and the global object in sloppy scripts. Calling with
new makes this the new object being created, and call, apply and bind set it explicitly. Taking a method off
its object, for example by passing cart.add as a callback, turns the later call into a plain call, so this is
lost. Arrow functions are the exception: they have no this of their own and use the this of the code around them,
whatever the call looks like.
Model answers open with any pass: ₹79 for 1 week ($2.99), one payment that never renews.
-
Mid to senior
A shared logging module exports an object whose info method reads this.prefix. A service writes const { info } = logger; info('started') and crashes with "Cannot read properties of undefined (reading 'prefix')". Explain the crash, then redesign the module so destructured methods work without breaking subclasses or tests that spy on methods.
-
Fresher to mid
A click handler reads this.dataset.id and works as addEventListener('click', function () {...}). Rewritten as an arrow function it reads undefined, and a teammate's fix, passing panel.onClick, a method that uses this.items, breaks the panel instead. Explain this in each version, and write a handler that does not depend on this.
Key takeaways
thisin a regular function is supplied by each call:obj.m()givesobj,f()givesundefinedin strict code andglobalThisin sloppy code, andnew F()gives the new object.- Only the shape of the call counts:
(obj.m)()andobj.m?.()keepobj;const f = obj.m; f()and(0, obj.m)()lose it. - An inherited method’s
thisis the object it was called on, not the prototype that holds it. - Top-level
thisisundefinedin ES modules,globalThisin classic scripts andmodule.exportsin CommonJS; writeglobalThiswhen you mean the global object. - Passing
obj.mas a callback losesthis. Fix it with an arrow at the call site, athisArg, a function bound once, or methods that do not depend onthis.
References
- ECMAScript 2026 Language Specification: the this keyword (Ecma International)
- ECMAScript 2026 Language Specification: EvaluateCall (Ecma International)
- ECMAScript 2026 Language Specification: OrdinaryCallBindThis (Ecma International)
- ECMAScript 2026 Language Specification: the grouping operator (Ecma International)
- ECMAScript 2026 Language Specification: the comma operator (Ecma International)
- ECMAScript 2026 Language Specification: GetThisBinding of module environment records (Ecma International)
- ECMAScript 2026 Language Specification: Array.prototype.forEach (Ecma International)
- HTML Living Standard: timers (WHATWG)
- Node.js v24 documentation: class Timeout (OpenJS Foundation (Node.js))
- Node.js v24 documentation: the module wrapper (OpenJS Foundation (Node.js))
- this (MDN Web Docs)
- Method definitions (MDN Web Docs)
Related tools
Report a problem with this lesson
Kept only in this browser. Your Learn progress