Topics
v5.0.0
On this page · sections

JavaScript Functions & this

Can you predict this from the call site — and know when an arrow function makes bind ineffective?

Functions are first-class values. A normal function does not permanently receive this from where it was defined. For ordinary functions, this is determined primarily by how the function is called.

Arrow functions are the important exception:

An arrow function does not have its own this; it captures this lexically from its surrounding scope.

This distinction explains many frontend bugs involving:

  • event handlers
  • callbacks
  • timers
  • class methods
  • call / apply / bind
  • constructors
  • React handlers
  • extracting methods from objects

The senior-level mental model is:

Normal function ↓ Look at the call site ↓ Determine this Arrow function ↓ Ignore the call site's this ↓ Use surrounding lexical this

Questions

Function Types

  1. Declaration vs Expression vs Arrow

this

  1. How this Is Determined
  2. call, apply, and bind

Senior-Level Follow-ups

  1. Arrows and thisArg
  2. new vs Bound Functions
  3. Detached Methods in Callbacks

Function Types

1. Declaration vs Expression vs Arrow

Why This Matters

Function syntax affects:

  • hoisting
  • whether the function has its own this
  • whether it can be used with new
  • whether it has its own arguments
  • whether it has a prototype property

Problem

What is the difference between these three forms?

function declared() {} const expressed = function () {}; const arrow = () => {};

Comparison

KindCallable before declaration line?Own dynamic this?Constructable with new?Own arguments?Own prototype?
Function declarationYesYesYesYesYes
Function expressionNoYesYesYesYes
Arrow functionNoNo — lexicalNoNoNo

“Callable before the line” is deliberately different from saying “hoisted.”

For example:

declared(); // works function declared() { return "ok"; }

But:

expressed(); // ReferenceError const expressed = function () {};

and:

arrow(); // ReferenceError const arrow = () => {};

The bindings for const exist before initialization, but they remain in the Temporal Dead Zone.

this Example

const obj = { value: 42, method() { return this.value; }, arrow: () => this.value, }; obj.method(); // 42 obj.arrow(); // depends on the surrounding lexical this

The important point is that method gets this === obj because it is called as:

obj.method();

The arrow does not.

Its this was determined by the surrounding scope where the arrow was created.

In an ES module, top-level this is undefined, so:

const obj = { arrow: () => this, }; obj.arrow(); // undefined

This is one reason arrow functions are generally inappropriate when you intentionally need a dynamically selected receiver.

Interview Answer

“Function declarations are hoisted and callable before their declaration. Function expressions and arrows are initialized where their binding is assigned. Ordinary functions have dynamic this, arguments, and can be constructable. Arrows have lexical this, no own arguments, and cannot be used with new. I use arrows when I want lexical this, and ordinary methods/functions when I need call-site-dependent this.”

this

2. How this Is Determined

Why This Matters

The safest way to answer a this question is not:

“What scope is this function in?”

Instead ask:

“How is this function being called?”

For ordinary functions, use this decision process.

Problem

What is this for each of these calls?

function show() { return this; } const obj = { show }; show(); obj.show(); show.call(obj); new show();

The Practical Priority List

For an ordinary function:

1. new ↓ new instance 2. call / apply / bind ↓ explicitly supplied this 3. obj.fn() ↓ obj 4. fn() ↓ undefined in strict mode global object in sloppy mode

Arrow functions are outside this decision tree:

arrow() ↓ lexical this from surrounding scope

They do not acquire a new this from the call site.

Rule 1: new

function User(name) { this.name = name; } const user = new User("Ada"); user.name; // "Ada"

When a constructable function is called with new, JavaScript creates a new object and invokes the constructor with that object as this.

Rule 2: Explicit Binding

function show() { return this.name; } const user = { name: "Ada", }; show.call(user); // "Ada"

Similarly:

show.apply(user); // "Ada"

and:

const boundShow = show.bind(user); boundShow(); // "Ada"

Rule 3: Method Call

const user = { name: "Ada", show() { return this.name; }, }; user.show(); // "Ada"

The relevant part is the call expression:

user.show()

not merely the fact that show happens to be stored inside user.

Rule 4: Bare Function Call

function show() { return this; } show();

In strict mode:

undefined

In a sloppy-mode script, the value is the global object.

ES modules are always strict, so module code gets:

undefined

for a bare call of an ordinary function.

The Classic Detached Method

This distinction is critical:

const user = { name: "Ada", show() { return this.name; }, }; user.show(); // "Ada"

But:

const fn = user.show; fn();

The function has been extracted from the object.

It is now a bare call:

user.show() ↓ this = user fn() ↓ this = undefined in strict mode

The function itself did not change.

The call site changed.

Interview Answer

“For an ordinary function, I inspect the call site. new creates the constructor receiver, explicit call/apply/bind supplies this, obj.fn() uses obj, and a bare call gives undefined in strict mode or the global object in sloppy mode. Arrow functions are different: they capture this lexically.”

3. call, apply, and bind

Why This Matters

These methods let you explicitly control this for ordinary functions.

The key difference is whether the function runs immediately.

Problem

What is the difference between call, apply, and bind?

Example

function greet(greeting, punctuation) { return `${greeting} ${this.name}${punctuation}`; } const user = { name: "Ada", };

call

Arguments are passed individually:

greet.call(user, "Hello", "!"); // "Hello Ada!"

The function executes immediately.

apply

Arguments are supplied as an array-like collection:

greet.apply(user, ["Hello", "!"]); // "Hello Ada!"

It also executes immediately.

bind

bind does not execute the function.

It creates a new function with a permanently associated this value for ordinary calls.

const boundGreet = greet.bind(user, "Hello"); boundGreet("!"); // "Hello Ada!"

You can also partially apply arguments:

const sayHello = greet.bind(user, "Hello"); sayHello("!"); // "Hello Ada!"

Conceptually:

call └── invoke now └── explicit this apply └── invoke now └── explicit this + argument list bind └── create new function ├── preset this └── optionally preset arguments

bind Is Not Just About this

This:

const fn = greet.bind(user, "Hello");

has two effects:

this → user first argument → "Hello"

This makes bind useful for both:

  • preserving a method receiver
  • partial application

Constructor Behavior

Bound functions have an important special case.

If a bound function is used with new, the bound thisArg is ignored:

function User(name) { this.name = name; } const BoundUser = User.bind( { ignored: true }, "Ada" ); const user = new BoundUser(); user.name; // "Ada"

The this used by the constructor is the newly created instance, not:

{ ignored: true }

The bound argument "Ada" is still supplied.

Interview Answer

“call and apply invoke immediately with an explicit this; their main difference is how arguments are supplied. bind creates a new function with preset this and optionally preset leading arguments. If that bound function is called with new, the bound thisArg is ignored and the newly constructed instance becomes this.”

Senior-Level Follow-ups

4. Arrows and thisArg

Why This Matters

Many array APIs accept a thisArg:

array.map(callback, thisArg)

That does not mean it can control this for an arrow function.

Ordinary Function

const values = [1, 2, 3]; const result = values.map( function (n) { return n * this.multiplier; }, { multiplier: 10 } ); console.log(result); // [10, 20, 30]

The callback is an ordinary function, so map can provide the thisArg.

Arrow Function

const values = [1, 2, 3]; const result = values.map( (n) => n * this.multiplier, { multiplier: 10 } );

The thisArg is ignored for purposes of the arrow’s this.

The arrow gets this from its surrounding lexical environment.

Why bind Does Not Fix an Arrow

This does not work:

const arrow = () => this.name; const bound = arrow.bind({ name: "Ada", }); bound(); // still uses the arrow's lexical this

bind can create a bound function, but it cannot replace the lexical this captured by an arrow.

This is a useful interview distinction:

Ordinary function ↓ dynamic this ↓ call/apply/bind can influence it Arrow function ↓ lexical this ↓ call/apply/bind cannot replace it

Interview Answer

“thisArg works with callbacks that have dynamic this. It doesn’t change an arrow’s lexical this. Likewise, bind cannot rebind an arrow’s this, because the arrow never has its own dynamically bound this in the first place.”

5. new vs Bound Functions

Why This Matters

This is a common senior-level follow-up because it tests whether you know that constructor calls have special this semantics.

Consider:

function User(name) { this.name = name; } const BoundUser = User.bind( { ignored: true }, "Ada" ); const user = new BoundUser(); console.log(user.name); // "Ada"

The bound object is not used as the constructor’s this.

Instead:

new BoundUser() ↓ create new instance ↓ constructor executes ↓ this → new instance

The bound argument is still preserved:

BoundUser ├── bound thisArg → ignored under new └── bound arguments → ["Ada"]

Therefore:

user.name; // "Ada"

instanceof

The bound function remains associated with its target constructor for instanceof purposes:

user instanceof User; // true

This works even though BoundUser itself does not provide a normal own prototype for instances to inherit from.

Interview Answer

“A bound constructor preserves its target function and partially applied arguments. When invoked with new, its bound thisArg is ignored because new creates the receiver. The resulting object is still an instance associated with the original constructor.”

6. Detached Methods in Callbacks

Why This Matters

One of the most common real-world this bugs is passing a method as a callback.

Consider:

const obj = { value: 42, handle() { console.log(this.value); }, }; button.addEventListener("click", obj.handle);

The callback is passed as a function value.

It is not equivalent to:

button.addEventListener("click", () => { obj.handle(); });

The first version gives the DOM event system control over the callback’s invocation.

For an ordinary function listener, addEventListener invokes the listener with this set to the EventTarget on which the listener was registered.

So in this case:

obj.handle

does not receive:

this === obj

Fix 1: Wrapper Function

button.addEventListener("click", () => { obj.handle(); });

Now the wrapper’s job is simply to perform:

obj.handle()

which provides obj as the receiver.

Fix 2: Bind the Method

button.addEventListener( "click", obj.handle.bind(obj) );

bind creates a new function whose ordinary-call this is fixed to obj.

One practical consequence:

button.removeEventListener( "click", obj.handle.bind(obj) );

does not remove the listener added earlier, because each bind() call creates a different function object.

Store the bound function if you need to remove it:

const boundHandle = obj.handle.bind(obj); button.addEventListener("click", boundHandle); // later button.removeEventListener("click", boundHandle);

Class Methods

Prototype methods can also lose their receiver:

class View { update() { console.log(this); } handle() { this.update(); } } const view = new View(); button.addEventListener("click", view.handle);

handle is extracted from view.

The callback is therefore no longer invoked as:

view.handle()

If handle needs the instance, bind it:

class View { constructor() { this.handle = this.handle.bind(this); } handle() { this.update(); } update() {} }

Class Fields With Arrow Functions

Another approach is an arrow function class field:

class View { handle = () => { this.update(); }; update() {} }

The arrow captures the instance’s lexical this, so extracting it does not lose the receiver:

const view = new View(); button.addEventListener("click", view.handle);

The trade-off is that the function is an own property of each instance, rather than a shared prototype method.

Conceptually:

Prototype method View.prototype.handle ↑ shared by instances Arrow class field instance #1 → own handle function instance #2 → own handle function instance #3 → own handle function

For most UI components this distinction is rarely the dominant performance concern, but it is worth understanding when designing large object graphs or performance-sensitive classes.

React Connection

In React function components, event handlers are normally written as arrows or function expressions that close over render-local values:

function Button({ user }) { const handleClick = () => { console.log(user.name); }; return <button onClick={handleClick}>Open</button>; }

Here, this is generally not the issue.

React function components do not rely on class-style dynamic this for handlers.

The important mechanism is the closure:

render ↓ user binding ↓ handleClick closure

This is why modern React code generally prefers closures and lexical variables over manually managing this.

Class components are different:

class Button extends React.Component { handleClick() { console.log(this.props.user); } }

Passing:

<button onClick={this.handleClick} />

can lose the instance receiver unless the method has been bound or defined as an arrow class field.

Interview Answer

“Passing obj.handle as a callback extracts the method from its receiver. The callback is no longer invoked as obj.handle(), so the original receiver isn’t automatically preserved. I can preserve it with a wrapper, bind, or an arrow class field. In modern React function components, closures largely replace the need for class-style this management.”

Final Interview Strategy

When you see a this question, don’t guess from the surrounding code.

Use this checklist:

1. Is it an arrow or ordinary function? ↓ 2. If ordinary, how is it being called? ↓ 3. Is there new? ↓ 4. Is there call/apply/bind? ↓ 5. Is it obj.fn()? ↓ 6. Otherwise, is it a bare call? ↓ 7. If it's an arrow, find its lexical surrounding this

The most important distinction is:

Ordinary function → this is determined dynamically Arrow function → this is captured lexically

And the most common production mistake is:

const fn = obj.method; fn();

because extracting a method changes the call site.

Quick Reference

Situationthis for ordinary function
new Fn()New instance
fn.call(obj)obj
fn.apply(obj)obj
fn.bind(obj)()obj
obj.fn()obj
fn() in strict modeundefined
fn() in sloppy modeGlobal object
Arrow functionLexical this

Remember:

call site → ordinary this lexical scope → arrow this

Senior-Level Signal

A weak answer is:

“this refers to the object.”

That’s often wrong.

A stronger answer is:

“For ordinary functions, this is determined by the call site. A method call supplies the receiver, explicit binding can supply it directly, new creates a new receiver, and a bare call gets undefined in strict mode.”

A senior answer continues:

“Arrow functions deliberately don’t participate in dynamic this; they capture it lexically. That’s why call, apply, bind, and array-method thisArg cannot replace an arrow’s this. In production, the important failure mode is losing a method’s receiver when passing it as a callback. I choose between a wrapper, bind, or an arrow class field based on the required lifecycle and ownership semantics.”

The key habit is simple:

Don’t ask “What is this here?” Ask “How was this function called?”

Last updated on