Course outline

JavaScript / Intermediate: modules and asynchronous contracts / Lesson 12

Prototypes, classes, and receiver behavior

Follow property lookup and this through ordinary objects and classes. Share behavior intentionally while protecting invariants and avoiding accidental changes to global prototypes.

About 4 min readContent reviewed
On this page

Lesson goals

  • Trace own and inherited property lookup.
  • Explain receiver-dependent method calls.
  • Use classes and composition with explicit invariants.

Lookup follows a chain

Reading a property first considers the object’s own properties, then follows its prototype chain until a matching property or the end is reached. A method can therefore be shared without copying its function onto each instance. A missing own property does not mean the read will be undefined, because an inherited property may satisfy it. Object.hasOwn asks the narrower ownership question. Keep dictionary data separate from inherited behavior, especially when external keys are involved. Avoid changing built-in prototypes to add application helpers; that global mutation can surprise unrelated code and make dependencies difficult to reason about.

A shared method uses its call receiver

For object.method(), the ordinary call supplies object as this. The method’s source location does not permanently bind it to the object from which it was first read. Assigning the function to a plain variable and calling it can lose the intended receiver; in module strict mode, an unbound ordinary function receives undefined as this. bind creates a function with a fixed receiver, while an arrow captures this from its lexical surroundings. Choose based on intent, particularly when passing methods as callbacks. Do not assume a class method automatically binds itself when extracted.

javascript Inherited behavior and an explicit bound receiver
const behavior = { describe() { return this.title; } };
const course = Object.create(behavior);
course.title = "DOM";
console.log(course.describe());
console.log(Object.hasOwn(course, "describe"));
const bound = course.describe.bind(course);
console.log(bound());
Expected output
DOM
false
DOM

describe is inherited but reads the receiver’s own title. bind preserves that receiver for a later standalone call.

Classes organize construction and behavior

A class gives syntax for construction and prototype-based methods with additional language rules. The constructor establishes valid initial state; methods preserve the intended transitions. Public fields are not a substitute for invariants, because callers can assign arbitrary values. Private fields restrict direct access but still need sensible methods and error policies. A private reservation count can prevent accidental outside assignment while reserve checks remaining capacity. Avoid exposing an internal mutable collection through a getter if callers should not bypass those checks. Class syntax does not remove identity or shallow-copy behavior learned for ordinary objects.

Test a stateful object through its public operations

The example starts with capacity two and rejects any reservation that would exceed it by returning false. The available getter derives its answer from private state instead of maintaining a second potentially contradictory counter. The caller receives a primitive, so it cannot edit the internal count through the returned value. This deliberately small model omits cancellation and concurrency; adding them requires new transition rules rather than assuming private fields make every workflow correct. Test the exact capacity boundary and repeated rejection to confirm a failed operation does not change the state.

javascript A bounded private counter
class Seats {
  #used = 0;
  constructor(capacity) { this.capacity = capacity; }
  reserve() {
    if (this.#used >= this.capacity) return false;
    this.#used++;
    return true;
  }
  get available() { return this.capacity - this.#used; }
}
const seats = new Seats(2);
console.log(seats.reserve(), seats.reserve(), seats.reserve());
console.log(seats.available);
Expected output
true true false
0

The third attempt has no effect. Production design should also validate and protect capacity if external mutation is not allowed.

Choose composition when it keeps dependencies clear

Inheritance can reuse a coherent behavioral contract, but deep hierarchies often force unrelated features into a shared ancestor. Composition passes a pricing policy, formatter, or storage adapter to an object that coordinates them. This makes substitution and tests explicit without relying on global prototype patches. A subclass must respect the expectations callers have of its base operations; changing success or error meanings can break substitution even if method names match. Prefer small domain operations and review the actual interaction contract before choosing class inheritance merely to avoid a few repeated lines.

Common errors and fixes

An extracted method reads undefined this

Why it happens
The callback call loses the intended receiver.
How to fix it
Bind the method or wrap a deliberate receiver call.
Prevent it next time
Test methods in the callback context that uses them.

Inherited fields are accepted as input data

Why it happens
A property read is confused with own-property validation.
How to fix it
Validate an allowed schema and use own-property checks where required.
Prevent it next time
Treat external dictionaries as a distinct boundary.

Private state is assumed to guarantee correctness

Why it happens
Access control substitutes for transition validation.
How to fix it
Validate construction and each allowed operation.
Prevent it next time
Test invariants at boundaries and after rejected operations.

Check your understanding

1. Why can obj.method() work while const f=obj.method; f() fails?

Show worked solution

The first ordinary call supplies obj as receiver; the second does not. In a module, this is undefined for an unbound ordinary call. Use obj.method.bind(obj) or () => obj.method() when that receiver is intended, and consider lifecycle ownership of the captured object.

2. How should the Seats example protect a fixed capacity?

Show worked solution

Validate capacity as a nonnegative safe integer in construction and store it in a private field or otherwise expose it read-only by contract. Test invalid construction and ensure callers cannot change the invariant by assigning a public capacity property. Privacy complements validation rather than replacing it.

Key takeaways

  • Property lookup and call receiver are separate mechanisms.
  • Encapsulation supports invariants only when operations preserve them.

Official references