IntermediateObjects, prototypes, classes, and this~45 min

Objects, Prototypes, Classes, and this

Property descriptors and getters/setters, how the prototype chain implements delegation, classes as syntax over that same mechanism, and how this is actually determined.

By the end of this lesson, you can

  • Explain what a property descriptor controls, and use a getter/setter pair
  • Trace a property lookup through the prototype chain, and explain what Object.create does
  • Write a class with public and private fields, and explain what it desugars to on the prototype
  • Determine what this refers to from a function's call site, including with call/apply/bind and arrow functions

Why it matters

Arrays and Objects treated objects as data containers. JavaScript’s object model goes deeper: every object has a hidden link to another object it delegates to, classes are a specific syntax built on top of that same mechanism, and this — one of the most frequently misunderstood parts of the language — is determined by a rule most languages don’t share.

Property descriptors and accessors

Every object property has more configuration than just its value — a property descriptor controls whether it’s writable, enumerable (shows up in for...in/Object.keys), and configurable (can be deleted or reconfigured):

const user = {};
Object.defineProperty(user, 'id', {
  value: 42,
  writable: false,
  enumerable: false,
});

user.id = 99; // silently ignored (or throws in strict mode)
console.log(Object.keys(user)); // [] — id doesn't enumerate
console.log(user.id); // 42 — still directly readable

A getter/setter pair defines a property computed on access, rather than stored directly:

const rectangle = {
  width: 10,
  height: 5,
  get area() {
    return this.width * this.height;
  },
  set area(value) {
    this.height = value / this.width; // keep width fixed, derive height
  },
};

console.log(rectangle.area); // 50 — computed, not stored
rectangle.area = 100;
console.log(rectangle.height); // 10

The prototype chain

Every object has an internal link — its prototype — to another object it delegates to. Reading a property that isn’t found on the object itself continues the search up this chain:

Where array methods actually live, traced up the prototype chain
StepObject checkedFound?
1[1, 2, 3] itselfNo own property called "map"
2Array.prototype (the array's prototype)Yes — map is defined here
3(search stops)Found, so Object.prototype is never reached

Object.create(proto) creates a new object with an explicit prototype — the mechanism underneath every higher-level pattern in this lesson:

const animal = {
  speak() {
    return `${this.name} makes a sound.`;
  },
};

const dog = Object.create(animal); // dog delegates to animal
dog.name = 'Rex';
console.log(dog.speak()); // "Rex makes a sound." — found via the prototype chain
console.log(Object.getPrototypeOf(dog) === animal); // true

Classes: syntax over the same mechanism

class Animal {
  #energy = 100; // private field — inaccessible outside this class entirely

  constructor(name) {
    this.name = name; // public field, set in the constructor
  }

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

  rest() {
    this.#energy = Math.min(100, this.#energy + 10);
    return this.#energy;
  }
}

class Dog extends Animal {
  speak() {
    return `${this.name} barks.`; // overrides Animal's speak
  }
}

const rex = new Dog('Rex');
console.log(rex.speak()); // "Rex barks."
console.log(rex instanceof Animal); // true — extends sets up the prototype chain
console.log(rex.#energy); // SyntaxError — private fields aren't accessible even syntactically outside the class

A class’s methods live on its prototype, exactly like the Object.create example above — class is standardized syntax for the same delegation mechanism, with private fields (#name) as a genuinely enforced feature the older patterns couldn’t provide.

this, precisely

this is not determined by where a function is defined — it’s determined by how it’s called, the “call site”:

Four ways a function can be called, and what this is each time
Call styleExamplethis is
Method callobj.method()obj — whatever precedes the dot
Plain callsomeFunction()undefined in strict mode/modules (the global object otherwise)
Constructor callnew SomeClass()The newly created instance
Explicit bindingfn.call(obj) / fn.apply(obj) / fn.bind(obj)obj — set explicitly
const counter = {
  count: 0,
  increment() {
    this.count++;
  },
};

counter.increment(); // this is counter — works
const detached = counter.increment;
detached(); // this is undefined here — throws, "Cannot read properties of undefined"

const bound = counter.increment.bind(counter);
bound(); // this is permanently counter, regardless of how bound() is later called

Arrow functions don’t have their own this at all — they use whatever this is in their enclosing lexical scope, exactly like any other variable, which makes them the natural choice for a callback that should keep the outer this:

const counter = {
  count: 0,
  incrementLater() {
    setTimeout(() => {
      this.count++; // arrow function — this is inherited from incrementLater, so this === counter
    }, 100);
  },
};

Check your understanding

What does this log? const obj = { value: 42, getValue: function () { return this.value; }, }; const detached = obj.getValue; console.log(detached());
Or reveal the answer without checking

Answer:A TypeError
detached() is a plain function call — no object precedes it — so this is undefined (in strict mode/modules). Reading this.value on undefined throws a TypeError. this is determined by the call site, not by where getValue was originally defined.

A Dog class extends an Animal class purely to reuse a bark() method that doesn't logically depend on being an Animal. What's the likely better design?
Or reveal the answer without checking

Answer:Composition — give Dog a barking behavior it uses, rather than modeling an is-a relationship that doesn't really hold
Inheritance should model a genuine is-a relationship. Reusing a method purely for convenience, without a real is-a relationship, is exactly the case where composition — combining smaller, focused pieces — produces a more flexible design than an inheritance hierarchy chosen for the wrong reason.

Practice: warm-up

Predict what each logs, then check:

const timer = {
  seconds: 0,
  startPlain: function () {
    setTimeout(function () {
      this.seconds++;
      console.log(this.seconds);
    }, 0);
  },
  startArrow: function () {
    setTimeout(() => {
      this.seconds++;
      console.log(this.seconds);
    }, 0);
  },
};

timer.startPlain();
timer.startArrow();

Stuck? Reveal one hint at a time.

  1. Hint 1

    startPlain's inner function is a plain function passed as a callback — it gets its own this when setTimeout eventually calls it.

  2. Hint 2

    startArrow's inner function is an arrow function — it has no this of its own, so it uses startArrow's this, which is timer.

Reveal the outcome

Try the problem yourself before reading this. There is often more than one reasonable approach — treat this as one worked example, not the only correct answer.

startPlain’s callback throws a TypeError (this is undefined, so this.seconds++ fails). startArrow’s callback logs 1 — its arrow function inherited this from startArrow, which was timer.

Practice: apply it

Using Object.create, build a vehicle prototype object with a describe() method returning "A vehicle.", then a car object that delegates to it but overrides describe() to return "A car.". Verify Object.getPrototypeOf(car) === vehicle.

Stuck? Reveal one hint at a time.

  1. Hint 1

    const car = Object.create(vehicle); creates car with vehicle as its prototype.

  2. Hint 2

    Defining describe directly on car (car.describe = function () {...}) shadows vehicle's version without modifying vehicle at all.

Reveal a reference solution

Try the problem yourself before reading this. There is often more than one reasonable approach — treat this as one worked example, not the only correct answer.

const vehicle = {
  describe() {
    return 'A vehicle.';
  },
};

const car = Object.create(vehicle);
car.describe = function () {
  return 'A car.';
};

console.log(car.describe()); // "A car."
console.log(Object.getPrototypeOf(car) === vehicle); // true

Summary

  • A property descriptor controls more than a property’s value — writability, enumerability, and configurability; getters/setters compute a property on access instead of storing it directly.
  • Every object delegates to another object via its prototype; a property lookup walks up this chain until found. Object.create(proto) creates that link explicitly.
  • Classes are syntax over the same prototype mechanism — methods live on the class’s prototype — with private fields (#name) as a genuinely new, enforced capability.
  • Prefer composition over inheritance when the relationship isn’t a genuine “is a” — a deep inheritance hierarchy chosen for convenience is a common source of fragile design.
  • this is determined by a function’s call site (how it’s called), not where it’s defined — a plain callback loses the surrounding this entirely, which is exactly what an arrow function callback avoids by having no this of its own.