Learn Frontend Part 6: this, Prototypes and Classes in JavaScript - call, apply and bind Explained

Learn Frontend Part 6: this, Prototypes and Classes in JavaScript - CodingHumans

The value of this is the single most confusing part of JavaScript for many developers, and prototypes are close behind. Both appear constantly in interviews as "what does this print?" questions, and both explain real bugs, such as a method that loses its object when passed as a callback.

This is Part 6 of the Learn Frontend series. We cover the four rules that decide this, how arrow functions differ, call, apply and bind, the prototype chain, and how ES6 classes sit on top of it.

1. The four rules for this

In a regular function, this is decided by how the function is called, not where it is written. Check these rules from highest to lowest priority:

  1. new binding: new User() creates a fresh object and sets this to it.
  2. Explicit binding: fn.call(obj), fn.apply(obj) or a function created with fn.bind(obj) uses obj.
  3. Implicit binding: obj.method() uses the object before the dot.
  4. Default binding: a plain call fn() gives undefined in strict mode (and in modules and classes), or the global object in sloppy mode.
'use strict';
const user = {
  name: 'Asha',
  hello() { return 'Hi ' + this.name; },
};

user.hello();                    // 'Hi Asha'   implicit: object before the dot
const fn = user.hello;
fn();                            // TypeError: this is undefined (default binding)
fn.call({ name: 'Ravi' });       // 'Hi Ravi'   explicit
const bound = fn.bind(user);
bound();                         // 'Hi Asha'   bound for good

function Person(name) { this.name = name; }
const p = new Person('Meera');   // new: this is the new object

2. Arrow functions and lost this

Arrow functions do not have their own this. They capture this from the surrounding scope at the moment they are created, and call, apply and bind cannot change it.

  • That makes arrows ideal for callbacks inside methods, such as setTimeout(() => this.save(), 1000).
  • It makes them a poor choice for object methods that need the object: { name: 'A', greet: () => this.name } does not see name.

Losing this: passing obj.method as a callback (to an event listener or setTimeout) detaches it from obj, so the call falls back to default binding. Fix it with obj.method.bind(obj), an arrow wrapper () => obj.method(), or class fields declared as arrow functions. In DOM event listeners written as regular functions, this is the element the listener is attached to.

3. call, apply and bind

MethodCalls immediately?Arguments
fn.call(thisArg, a, b)YesOne by one
fn.apply(thisArg, [a, b])YesAs an array
fn.bind(thisArg, a)No, returns a new functionPre-filled (partial application)

"Write a polyfill for bind" is a classic interview task that tests closures, this and rest/spread together:

Function.prototype.myBind = function (thisArg, ...preset) {
  const original = this;                       // the function being bound
  return function (...later) {
    return original.apply(thisArg, [...preset, ...later]);
  };
};

function greet(greeting, punct) { return greeting + ', ' + this.name + punct; }
const hi = greet.myBind({ name: 'Dev' }, 'Hello');
console.log(hi('!'));                          // 'Hello, Dev!'

4. Prototypes and the prototype chain

Every object has a hidden link, [[Prototype]], to another object (readable with Object.getPrototypeOf(obj)). When you read a property that the object does not have, JavaScript walks up this prototype chain until it finds it or reaches null.

  • Arrays get map and push from Array.prototype; every object gets toString from Object.prototype.
  • Functions have a prototype property; objects created with new Fn() link to Fn.prototype, so methods placed there are shared by all instances instead of being copied.
  • Object.create(proto) makes a new object linked directly to proto.
  • obj.hasOwnProperty(key) (or Object.hasOwn) checks only the object itself, not its chain.
function Animal(name) { this.name = name; }
Animal.prototype.speak = function () { return this.name + ' makes a sound'; };

const dog = new Animal('Rex');
dog.speak();                                       // found on Animal.prototype
Object.getPrototypeOf(dog) === Animal.prototype;   // true
dog.hasOwnProperty('speak');                       // false

// The same thing with class syntax
class Cat extends Animal {
  #lives = 9;                                      // private field
  speak() { return super.speak() + ' (meow)'; }
  get lives() { return this.#lives; }
}

5. ES6 classes

class syntax is a cleaner way to write constructor functions and prototype methods; underneath it is still prototypes.

  • constructor runs on new; methods are placed on the prototype.
  • extends sets up the prototype chain; super(...) must be called before using this in a subclass constructor.
  • static members live on the class itself, not on instances.
  • #private fields and methods are truly private to the class.
  • Class bodies always run in strict mode, so a detached method call gives this === undefined.

Modern React uses function components and hooks instead of classes (Part 10), but classes still appear in libraries, error boundaries, interviews and other frameworks.

Interview tip: for any "what is this?" question, find the call site first, then apply the four rules in order. Remember that arrows simply look outwards.
Key takeaways
  • In regular functions, this depends on the call: new, then explicit (call/apply/bind), then implicit (obj.method()), then default.
  • Arrow functions capture this from the surrounding scope and cannot be rebound.
  • Passing obj.method as a callback loses this; fix it with bind or an arrow wrapper.
  • Objects inherit through the prototype chain; shared methods live on the prototype.
  • ES6 classes are syntax over prototypes, with extends, super, static and #private members.

Practice problems

Predict each output before running it in the console. These mirror real interview questions.

  1. Predict the output of calling an object method with setTimeout(obj.method, 0), then fix it two ways.
  2. Predict what an arrow function method on an object literal returns for this.name.
  3. Implement myCall and myApply polyfills.
  4. Implement the myBind polyfill and make it work with new.
  5. Create a three-level prototype chain with Object.create and trace a property lookup.
  6. Rewrite a constructor function plus prototype methods as an ES6 class with a private field.

Interview questions and FAQ

How is the value of this determined?

For regular functions it depends on the call site: a new call binds the new object, call/apply/bind bind the given object, a method call binds the object before the dot, and a plain call gives undefined in strict mode or the global object otherwise.

What is the difference between call, apply and bind?

call and apply invoke the function immediately with a chosen this; call takes arguments individually and apply takes an array. bind returns a new function with this (and optionally some arguments) permanently set.

Why do arrow functions not have their own this?

By design, arrows use lexical this, taking it from the enclosing scope. This makes them convenient for callbacks inside methods but unsuitable as object methods or constructors.

What is prototypal inheritance?

Objects link to other objects through their prototype. When a property is missing on an object, JavaScript looks it up along the prototype chain, so objects can share behaviour without copying it.

Are JavaScript classes real classes?

They are syntactic sugar over constructor functions and prototypes. They add conveniences such as extends, super, static members and private fields, but inheritance still works through the prototype chain.

Comments

Answered 0/0Correct 0