Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

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.

  • Intermediate
  • 20 minutes
  • Examples run with Node.js 24.21.0 and quickjs 0.32.0
  • By MySmartCoPilot
Free lesson, Pro answers: 20 minutes
Free: the whole lesson. With a pass: the model answers to its 2 interview questions.

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, seven calls, three different values of this JavaScript · lost-this.mjs
// 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

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']() and counter.whoAmI?.() all call a property of counter, so this is counter. 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 call f() has nothing before a dot, so it is a plain call and this is undefined. 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, so this is undefined. Build tools often write calls to imported functions in exactly this shape, because such a call must not pass a this.
  • [counter.whoAmI][0]() is a method call too, on the array: the object before the brackets is the array, so this is 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

Plain call, new, an inherited method and a nested function JavaScript · four-rules.mjs
// 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

A chain of four questions about how a function is called, each with the value this takes when the answer is yes.An arrow function?Called with new?Bound with bind(),or called by call()or apply()?Called asobj.method()?A plain call: f()this of the codearound the arrowthe new objectthe value thatwas givenobj, the objectbefore the dotundefined in strictcode and modules;globalThis insloppy scriptsnonononoyesyesyesyes

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`.

  1. Is the function an arrow function? Then `this` is the `this` of the code around the arrow, whatever the call looks like.
  2. Is it called with `new`? Then `this` is the new object being created.
  3. 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.
  4. Is it called as a method, `obj.method()`? Then `this` is `obj`, the object before the dot.
  5. Otherwise it is a plain call, `f()`. Then `this` is `undefined` in strict code, which includes modules and classes, and `globalThis` in sloppy scripts.
  1. Arrow function: this comes from the code around it, whatever the call looks like. viaArrow inside start() returns the timer’s label because it uses start’s this.
  2. new: new Ticket('B2') creates an object and runs Ticket with this set to it.
  3. Explicit: a function made by bind(), or called through call() or apply(), gets the this it was given. The next lesson covers these three.
  4. Method call: obj.method() gets obj. It is the object before the dot, not the object where the method was found: admin.greet() runs a method that lives on user, the prototype, yet this is admin, so it greets Ravi. Getters follow the same rule, which is why admin.full reads Ravi Rao.
  5. Plain call: undefined in strict code, globalThis in sloppy code. viaFunction() inside start() is a plain call, so it gets undefined even though it is written inside a method. Being nested says nothing about this; 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:

this at the top level of a module and of a classic script JavaScript · top-level-this.mjs
// 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

this at the top level of a CommonJS file JavaScript · top-level-this.cjs
// 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 this is undefined. The specification defines it that way for module code, and modules are strict, so plain calls inside them get undefined as well.
  • In a classic script, a browser <script> tag without type="module", top-level this is globalThis, the window. Sloppy functions in that script get globalThis from plain calls; strict ones get undefined.
  • In a CommonJS file, Node.js wraps the file’s code in a function before running it and calls that function with module.exports as this, so top-level this is 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:

cart.add as a callback, and four fixes JavaScript · callbacks.mjs
// 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

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 with cart before the dot. It is the clearest fix when you write the call yourself, and it leaves the method an ordinary method.
  • The thisArg parameter of forEach, map, filter, some, every and find: 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 with removeEventListener, which an arrow written inline at each call site does not allow.
  • No this at 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.
Ways to keep this when a method becomes a callback
Criterion Arrow at the call sitethisArgbind onceBound or closure methods
Where the fix lives Each callerEach callerOne shared functionThe object itself
Works with any API YesOnly APIs with a thisArgYesYes
Same function every time, so it can be removed NoYesYesYes
Cost A small function per call siteNoneOne bound functionOne function per method per object
When to choose Calls you write yourselfArray methodsEvent listeners you must remove laterObjects 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.

JavaScript & TypeScript Online Compiler Paste lost-this.mjs, add your own call shapes, such as an assignment inside parentheses, and predict each line first.

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 } to items and returns the cart, so calls can be chained.
  • remove(sku) removes every item with that sku and returns the cart.
  • total(prices) returns the sum of prices[sku] * qty over the items.
  • clear() empties items; clearLater(ms) calls clear() after ms milliseconds.
  • 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.

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.

  1. Question 1 of 6 What does this module print?

    Read the code, then choose one answer.

    const user = {
      name: 'Asha',
      hi() {
        return this?.name;
      },
    };
    const hi = user.hi;
    
    console.log(user.hi(), hi());
    Show the answer to question 1

    Answer: Asha undefined

    user.hi() is a method call, so this is user. hi() calls the same function with nothing before a dot, so in a module, which is strict code, this is undefined; the ?. turns the missing name into undefined instead of a TypeError.

  2. Question 2 of 6 What does this module print?

    Read the code, then choose one answer.

    const user = {
      name: 'Asha',
      hi() {
        return this?.name;
      },
    };
    
    console.log((user.hi)(), (0, user.hi)());
    Show the answer to question 2

    Answer: Asha undefined

    Parentheses around user.hi keep the property reference, so the call still has user before the dot. The comma operator returns the plain function value, so (0, user.hi)() is a plain call and this is undefined. Build tools write calls this way on purpose when they must not pass a this.

  3. Question 3 of 6 What does this module print?

    Read the code, then choose one answer.

    const timer = {
      label: 'T1',
      run() {
        function inner() {
          return this;
        }
        return inner();
      },
    };
    
    console.log(timer.run());
    Show the answer to question 3

    Answer: undefined

    inner is a regular function, and inner() is a plain call, so it gets its own this, which is undefined in module code. Being written inside a method does not matter. An arrow, const inner = () => this, would return timer.

  4. Question 4 of 6 const admin = Object.create(user); admin.name = 'Ravi'; where user.hi() returns this.name and user.name is 'Asha'. What does admin.hi() return?

    Choose one answer.

    Show the answer to question 4

    Answer: 'Ravi': this is admin, the object before the dot, even though hi lives on user

    this is the object the method was called on, not the object that holds the method. The lookup finds hi on user, the prototype, but the call is admin.hi(), so this.name reads admin.name.

  5. Question 5 of 6 What does console.log(this) print at the top level of an ES module, outside any function?

    Choose one answer.

    Show the answer to question 5

    Answer: undefined

    The specification defines this at the top level of a module as undefined. A classic script gets globalThis, and a CommonJS file in Node.js gets module.exports, because Node.js wraps it in a function.

  6. Question 6 of 6 totals.add(n) does this.sum += n. Which of these add 1 and 2 to totals.sum? Choose every right one.

    Choose every answer that is right.

    Show the answer to question 6

    Answer:

    • [1, 2].forEach(totals.add.bind(totals))
    • [1, 2].forEach((n) => totals.add(n))
    • [1, 2].forEach(totals.add, totals)

    forEach takes the value to use as this as its second argument; an arrow calls the method with totals before the dot; bind returns a function whose this is fixed. call runs add at once, with no n, and passes its return value, undefined, to forEach, which throws a TypeError because undefined is not a function.

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.

  1. 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.

    Model answer, Pro. Model answers open with any pass; prices are in the notice on this page. First step and follow-ups

  2. 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.

    Model answer, Pro. First step and follow-ups

Key takeaways

  • this in a regular function is supplied by each call: obj.m() gives obj, f() gives undefined in strict code and globalThis in sloppy code, and new F() gives the new object.
  • Only the shape of the call counts: (obj.m)() and obj.m?.() keep obj; const f = obj.m; f() and (0, obj.m)() lose it.
  • An inherited method’s this is the object it was called on, not the prototype that holds it.
  • Top-level this is undefined in ES modules, globalThis in classic scripts and module.exports in CommonJS; write globalThis when you mean the global object.
  • Passing obj.m as a callback loses this. Fix it with an arrow at the call site, a thisArg, a function bound once, or methods that do not depend on this.

References

Related tools

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.