Week 4: this, Objects & Classes

this is the other idea, alongside closures, that trips up almost everyone arriving from another language — because unlike most languages, JavaScript decides what this means at call time, not at definition time. This week nails down that rule for good, then builds up through object literals, prototypes and class syntax — the vocabulary every object-oriented pattern in JS rests on.

Phase 2 of 8 Week 4 of 14 ~3–4 Hours Hands-on Exercise Included

By the end of this week, you'll be able to

  • Predict what this refers to in a regular function, a method call, and an arrow function
  • Explain what the prototype chain is, and how it's different from real inheritance in other languages
  • Write a class with a constructor, methods, and a subclass using extends

1. How this Is Determined

In most languages, this (or its equivalent) always refers to the instance a method was defined on. In JavaScript, this is set fresh every time a function is called, based entirely on how it was called — not where it was written:

this depends on the call site
const user = {
  name: "Priya",
  greet() {
    console.log(`Hi, I'm ${this.name}`);
  },
};

user.greet(); // "Hi, I'm Priya" -- called AS a method of user, so this === user

const detachedGreet = user.greet;
detachedGreet(); // "Hi, I'm undefined" -- called plainly, this is no longer user

greet is the exact same function in both calls — what changed is how it was invoked. Calling user.greet() sets this to whatever comes immediately before the dot. Pulling the same function out into a plain variable and calling it directly loses that connection entirely; in a browser or Node module, this ends up undefined (in strict mode, which ES modules and classes always are).

arrow functions: this is inherited, not set
const user = {
  name: "Priya",
  greetLater() {
    setTimeout(() => {
      // an arrow function has no `this` of its own -- it uses this
      // from wherever it was WRITTEN, i.e. inside greetLater
      console.log(`Hi, I'm ${this.name}`);
    }, 100);
  },
};

user.greetLater(); // "Hi, I'm Priya", even inside the callback

An arrow function deliberately has no this of its own — it looks up this lexically, exactly like a normal variable, from the nearest enclosing regular function. That's precisely why arrow functions are the standard choice for callbacks that need to keep using the outer this, like the setTimeout callback above.

Never use an arrow function as an object method

An arrow function assigned as a method (greet: () => {'{'}...{'}'}) would look up this from outside the object literal entirely — usually the module scope — never the object itself. Regular methods (greet() {'{'}...{'}'}) are what correctly bind this to the calling object.

2. call, apply & bind

These three methods, available on every function, let you control what this is explicitly, instead of relying on the call site:

call & apply: invoke immediately with a chosen this
function greet(greeting) {
  console.log(`${greeting}, ${this.name}`);
}

const user = { name: "Priya" };

greet.call(user, "Hello");        // "Hello, Priya" -- arguments listed individually
greet.apply(user, ["Hello"]);     // "Hello, Priya" -- arguments passed as an array
bind: create a new function with this locked in
const greetPriya = greet.bind(user); // doesn't call it -- returns a new function

greetPriya("Hi");    // "Hi, Priya"
greetPriya("Hello"); // "Hello, Priya" -- this stays locked to user every time

call and apply do the same thing — invoke the function immediately with a specified this — and differ only in how the remaining arguments are passed (individually vs. as an array). bind is different: it doesn't call the function at all, it returns a new function with this permanently locked to whatever was passed in, useful for passing a method somewhere else (like an event handler) without losing its intended this.

3. Object Literals & Property Shorthand

Modern JavaScript has several shortcuts for writing objects that are worth knowing cold, because they show up in essentially every real codebase:

shorthand syntax
const name = "Priya";
const age = 29;

// Old way: repeating the key and the variable name
const userOld = { name: name, age: age };

// Property shorthand: when the key and variable name match, write it once
const user = { name, age };

// Method shorthand: no `function` keyword needed
const userWithMethod = {
  name,
  greet() {
    return `Hi, I'm ${this.name}`;
  },
};

// Computed property names: use a variable/expression AS a key
const field = "email";
const dynamicUser = { [field]: "priya@example.com" };
console.log(dynamicUser.email); // "priya@example.com"

Property shorthand and method shorthand are almost always used by default in modern code — the longer forms exist mainly so you can recognize them in older code. Computed property names matter whenever the key itself needs to come from a variable rather than being a fixed, known string.

4. Prototypes & the Prototype Chain

Every object in JavaScript has a hidden internal link to another object — its prototype — and property lookups that don't find a match on the object itself keep searching up that chain:

the prototype chain
const animal = {
  eats: true,
  describe() {
    return `This animal eats: ${this.eats}`;
  },
};

const rabbit = Object.create(animal); // rabbit's prototype is animal
rabbit.jumps = true;

console.log(rabbit.jumps);        // true -- rabbit's own property
console.log(rabbit.eats);         // true -- not on rabbit, found on its prototype (animal)
console.log(rabbit.describe());   // "This animal eats: true" -- describe() is inherited too

rabbit doesn't have an eats property of its own — when you access rabbit.eats, JavaScript doesn't find it on rabbit, so it checks rabbit's prototype (animal), finds it there, and returns it. This is prototypal inheritance: objects inherit directly from other objects, rather than from a fixed template class the way inheritance works in most other object-oriented languages.

class syntax is built on exactly this

You'll rarely write Object.create directly in everyday code — the class keyword in the next section is syntax sugar over this same prototype mechanism. Understanding the chain first is what makes class and extends click instead of feeling like a black box borrowed from another language.

5. Class Syntax

class gives object-oriented code a familiar, readable shape — under the hood, it's still prototypes, just with far less boilerplate:

a class with a constructor & methods
class Animal {
  constructor(name) {
    this.name = name; // runs once, when a new instance is created
  }

  describe() {
    return `${this.name} makes a sound.`;
  }
}

const dog = new Animal("Rex");
console.log(dog.describe()); // "Rex makes a sound."
inheritance with extends & super
class Dog extends Animal {
  constructor(name, breed) {
    super(name); // must call the parent's constructor before using `this`
    this.breed = breed;
  }

  describe() {
    // reuse the parent's version, then add to it
    return `${super.describe()} It's a ${this.breed}.`;
  }
}

const rex = new Dog("Rex", "Labrador");
console.log(rex.describe()); // "Rex makes a sound. It's a Labrador."

extends makes Dog inherit from Animal — concretely, it wires Dog.prototype's prototype to Animal.prototype, the exact chain from the previous section, just built automatically. super(...) calls the parent class's constructor and is required before this can be used in a subclass constructor; super.describe() calls the parent's version of a method you're overriding, letting you extend behavior instead of replacing it outright.

6. Hands-on Exercise

Hands-on

Model a small library with classes

Practice constructors, methods, inheritance and this together.

Requirements:

  1. Write a Book class with a constructor accepting title and author, and a method describe() returning a string like "1984 by George Orwell".
  2. Write an EBook class that extends Book, adds a fileSizeMB property in its own constructor (calling super() first), and overrides describe() to append the file size using super.describe().
  3. Create a Library class with a private-by-convention array of books, and methods addBook(book) and listBooks() that returns each book's describe() output as an array of strings.
  4. Add at least one Book and one EBook instance to a Library, then log the result of listBooks().
  5. Extract library.addBook into its own variable and call it directly (detached from library) — observe and note in a comment what breaks and why.
Hint

Step 5 is deliberately reproducing the detachedGreet problem from earlier in this lesson — the fix, if you want it to keep working, is const addBook = library.addBook.bind(library).

7. Knowledge Check

Four quick questions. Expand each to check your answer.

Q1

Given const fn = obj.method; fn();, why might this inside method not be obj anymore?

this is set by how a function is called, not by where it was defined. obj.method() sets this to obj because of the dot syntax at the call site; once method is copied into the plain variable fn and called as fn(), that connection is gone — this becomes undefined (in strict mode/modules) or the global object otherwise.

Q2

What's the difference between fn.call(obj, a, b) and fn.bind(obj)(a, b)?

call invokes fn immediately with this set to obj. bind doesn't call anything — it returns a brand-new function with this permanently locked to obj, which can then be called (or passed elsewhere, like an event handler) any number of times later. The end result of both lines above is the same call, but bind is for reuse; call is for a single, immediate invocation.

Q3

Given rabbit = Object.create(animal), why does rabbit.eats work even though rabbit never had an eats property set on it directly?

Property lookups that don't find a match on the object itself continue searching up its prototype chain. rabbit's prototype is animal, which does have an eats property, so the lookup finds it there and returns it — the same mechanism a class/extends hierarchy uses underneath.

Q4

In a subclass constructor, why must super(...) be called before using this?

super(...) runs the parent class's constructor, which is what actually initializes this for the instance being created. JavaScript enforces the call order directly — accessing this in a subclass constructor before calling super() throws a ReferenceError, because this genuinely doesn't exist yet at that point.