this (first look)
Predict what this refers to in method calls, plain calls and arrow functions, and fix the common ways of losing it with bind.
What you will learn
- Say what this is in a method call and in a plain function call
- Explain how arrow functions treat this
- Recognise the situations that lose this
- Fix a lost this with an arrow wrapper, bind, call or apply
On this page
Inside a function, the keyword this refers to some object, and which one depends on how the function is called, not where it is written. That is the opposite of how variables work, and it is the source of a well-known class of bugs. This lesson covers the four cases you will meet in everyday code. Methods live on objects, so here is the one idea about objects you need: an object literal such as { name: "Ada", greet() { ... } } is a bundle of named values in braces, and a function stored in one is called a method. A list in square brackets, such as [1, 2, 3], is an array. Lesson 26 teaches objects properly, and lesson 23 teaches arrays. The fundamentals lesson on records and objects gives the general idea, and classes, which use this a lot, come in a later module.
All code in this course runs as ES modules, which are always in strict mode. The rules below are the strict-mode rules.
Method calls: the object before the dot
When you call a function as object.method(), this inside is object.
const user = { name: "Ada", greet() { return `Hello, I am ${this.name}`; },};
console.log(user.greet());
const other = { name: "Grace", greet: user.greet };console.log(other.greet());
function whoAmI() { return this;}console.log(whoAmI());console.log(user.greet.call({ name: "Linus" }));Hello, I am AdaHello, I am GraceundefinedHello, I am Linususer.greet() has this set to user. The method was copied to other and called as other.greet(), so this is now other. The function does not belong to either object. Only the call decides.
whoAmI() is a plain call, with nothing before the dot. In strict mode this is undefined there. (In sloppy-mode scripts it was the global object, which is one reason strict mode is better.) The last line sets this explicitly with call, which comes back below.
The greet() { ... } form is the method shorthand for properties that hold functions.
Losing this
Here is the common bug. If you take a method out of its object, the call has nothing before the dot, and this is gone.
const user = { name: "Ada", greet() { return `Hello, I am ${this.name}`; },};
const detached = user.greet;try { console.log(detached());} catch (error) { console.log(`${error.name}: ${error.message}`);}TypeError: Cannot read properties of undefined (reading 'name')A method handed to another function as a callback loses this in the same way:
function runLater(callback) { return callback();}try { runLater(user.greet);} catch (error) { console.log(`${error.name}: ${error.message}`);}TypeError: Cannot read properties of undefined (reading 'name')Both errors say the same thing: this is undefined, so this.name cannot be read. The first is a method saved in a variable. The second is a method passed as a callback, where the receiving function calls it plainly. Callbacks are where this goes wrong most often. For example, setTimeout(user.greet, 100) looks right, but the timer calls the method without user, so the method no longer sees the user object.
Fixing it: bind, arrows and call
There are three fixes.
- Wrap the call in an arrow function that does the
object.method()call itself. bindreturns a new function withthisfixed permanently.callandapplycall a function once with a chosenthis.
const user = { name: "Ada", greet() { return `Hello, I am ${this.name}`; },};
function runLater(callback) { return callback();}
console.log(runLater(user.greet.bind(user)));console.log(runLater(() => user.greet()));
const bound = user.greet.bind({ name: "Grace" });console.log(bound());console.log(bound.call({ name: "ignored" }));Hello, I am AdaHello, I am AdaHello, I am GraceHello, I am Graceuser.greet.bind(user) and () => user.greet() both work as callbacks. A bound function cannot be re-bound: bound keeps { name: "Grace" } even when call tries to give it another this.
call and apply differ only in how they take the arguments: one by one, or in an array. bind can also fix some arguments, as in the partial application from the previous lesson.
function introduce(greeting, punctuation) { return `${greeting}, I am ${this.name}${punctuation}`;}
const ada = { name: "Ada" };
console.log(introduce.call(ada, "Hello", "!"));console.log(introduce.apply(ada, ["Hi", "?"]));
const adaSays = introduce.bind(ada, "Hey");console.log(adaSays("."));Hello, I am Ada!Hi, I am Ada?Hey, I am Ada.Arrow functions and this
An arrow function has no this of its own. It uses the this of the code where it was written, as a variable from an outer scope. This is exactly what you want for a callback inside a method.
const timer = { label: "clock", ticks: [1, 2, 3], describeWithFunction() { return this.ticks.map(function (t) { return `${this?.label}:${t}`; }); }, describeWithArrow() { return this.ticks.map((t) => `${this.label}:${t}`); }, arrowAsMethod: () => typeof this,};
console.log(timer.describeWithFunction());console.log(timer.describeWithArrow());console.log(timer.arrowAsMethod());[ 'undefined:1', 'undefined:2', 'undefined:3' ][ 'clock:1', 'clock:2', 'clock:3' ]undefined- In
describeWithFunction, the callback tomapis a plain function, so itsthisisundefined. Thethis?.labelshows that asundefinedwherethis.labelwould have thrown. - In
describeWithArrow, the arrow takes thethisof the method around it, which istimer, so the labels are right. arrowAsMethodis the opposite mistake: an arrow used as a method. Itsthisis whateverthisis at the top of the module, which isundefined. It does not get the object. Do not write methods as arrow functions. Use the shorthand.
The rule of thumb: use method shorthand or function for methods that use this, and arrows for callbacks inside them.
Common pitfalls
- Passing
obj.methodas a callback. It arrives withoutobj. Passobj.method.bind(obj)or() => obj.method(). - Assigning a method to a variable and calling it. Same problem.
- Using an arrow function as an object method. Its
thisis not the object. - Using a regular function as a callback inside a method. The callback gets its own
this. Use an arrow. - Forgetting that
thisdepends on the call. Read how the function is called, not where it is defined. - Re-binding a bound function. The first
bindwins.
Exercises
This program has five problems with this. Run it, then fix each one without changing the shape of the data. Bug 5 does not throw, but the answer is still wrong. After your fixes the output should read bug 1: 2, bug 2: [ 4, 6 ], bug 3: [ 'item a', 'item b' ], bug 4: I am logger and bug 5: 8.
const counter = { count: 0, step: 2, increment() { this.count += this.step; return this.count; },};
function attempt(label, fn) { try { console.log(label, fn()); } catch (error) { console.log(label, `${error.name}`); }}Bugs 1 to 3:
// Bug 1: method copied into a variable.const inc = counter.increment;attempt("bug 1:", () => inc());
// Bug 2: method passed as a callback.function twice(fn) { return [fn(), fn()];}attempt("bug 2:", () => twice(counter.increment));
// Bug 3: regular function inside a method.const report = { prefix: "item", list(items) { return items.map(function (item) { return `${this.prefix} ${item}`; }); },};attempt("bug 3:", () => report.list(["a", "b"]));bug 1: TypeErrorbug 2: TypeErrorbug 3: TypeErrorBugs 4 and 5:
// Bug 4: arrow function as a method.const logger = { name: "logger", describe: () => `I am ${this.name}`,};attempt("bug 4:", () => logger.describe());
// Bug 5: method called through another object's property.const wrapper = { run: counter.increment };attempt("bug 5:", () => wrapper.run());bug 4: TypeErrorbug 5: NaNShow solution
incis a detached method. Call it on the object:counter.increment().twice(counter.increment)passes the bare method. Passcounter.increment.bind(counter).- The callback to
mapis a regular function with its ownthis. Make it an arrow function. describeis an arrow, sothisis notlogger. Use the method shorthand.wrapper.run()hasthisset towrapper, which has nocountorstep, so the sum isNaN. Bind the method tocounterwhen it is stored.
const counter = { count: 0, step: 2, increment() { this.count += this.step; return this.count; },};
function attempt(label, fn) { try { console.log(label, fn()); } catch (error) { console.log(label, `${error.name}`); }}Fixes 1 to 3:
// Fix 1: call it on the object.attempt("bug 1:", () => counter.increment());
// Fix 2: bind the method.function twice(fn) { return [fn(), fn()];}attempt("bug 2:", () => twice(counter.increment.bind(counter)));
// Fix 3: use an arrow function so it keeps the method's this.const report = { prefix: "item", list(items) { return items.map((item) => `${this.prefix} ${item}`); },};attempt("bug 3:", () => report.list(["a", "b"]));bug 1: 2bug 2: [ 4, 6 ]bug 3: [ 'item a', 'item b' ]Fixes 4 and 5:
// Fix 4: use method syntax.const logger = { name: "logger", describe() { return `I am ${this.name}`; },};attempt("bug 4:", () => logger.describe());
// Fix 5: bind it to the object that owns the data.const wrapper = { run: counter.increment.bind(counter) };attempt("bug 5:", () => wrapper.run());bug 4: I am loggerbug 5: 8The counter state carries on between the steps, which is why bug 2 gives [ 4, 6 ]: the first fix already added 2.
Here is a program that makes six calls to the same hello method in different ways. Before you run it, write down what each line will print. Run it to check yourself. Five lines print a name, and one prints an error name instead.
const robot = { name: "Robo", hello() { return this.name; },};const copy = { name: "Copy", hello: robot.hello };const loose = robot.hello;const bound = robot.hello.bind({ name: "Bound" });
function attempt(label, fn) { try { console.log(label, fn()); } catch (error) { console.log(label, error.name); }}And the six calls:
attempt("1:", () => robot.hello());attempt("2:", () => copy.hello());attempt("3:", () => loose());attempt("4:", () => bound());attempt("5:", () => bound.call({ name: "Other" }));attempt("6:", () => robot.hello.call({ name: "Call" }));Show solution
const robot = { name: "Robo", hello() { return this.name; },};const copy = { name: "Copy", hello: robot.hello };const loose = robot.hello;const bound = robot.hello.bind({ name: "Bound" });
function attempt(label, fn) { try { console.log(label, fn()); } catch (error) { console.log(label, error.name); }}The six calls:
attempt("1:", () => robot.hello());attempt("2:", () => copy.hello());attempt("3:", () => loose());attempt("4:", () => bound());attempt("5:", () => bound.call({ name: "Other" }));attempt("6:", () => robot.hello.call({ name: "Call" }));1: Robo2: Copy3: TypeError4: Bound5: Bound6: Callrobot.hello(): the object before the dot isrobot.copy.hello(): the same function, but the object before the dot iscopy.loose(): nothing before the dot, sothisisundefinedandthis.namethrows aTypeError.bound():bindfixedthisto the{ name: "Bound" }object.bound.call(...): a bound function ignores a newthis, so it is stillBound.robot.hello.call(...):callsetsthisfor this one call, so the result isCall.
Summary
thisis set by how a function is called. Inobject.method()it isobject. In a plain call in strict mode it isundefined.- Taking a method out of its object, or passing it as a callback, loses
this. - Fix it with an arrow wrapper,
bind, orcallandapplyfor one-off calls. - Arrow functions use the
thisof their surroundings. They suit callbacks inside methods, not methods themselves.
That completes the module on functions. Next up: arrays, objects and data.