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:
| Step | Object checked | Found? |
|---|---|---|
| 1 | [1, 2, 3] itself | No own property called "map" |
| 2 | Array.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”:
| Call style | Example | this is |
|---|---|---|
| Method call | obj.method() | obj — whatever precedes the dot |
| Plain call | someFunction() | undefined in strict mode/modules (the global object otherwise) |
| Constructor call | new SomeClass() | The newly created instance |
| Explicit binding | fn.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
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.
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.
Hint 1
startPlain's inner function is a plain function passed as a callback — it gets its own this when setTimeout eventually calls it.
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.
Hint 1
const car = Object.create(vehicle); creates car with vehicle as its prototype.
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); // trueSummary
- 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.
thisis determined by a function’s call site (how it’s called), not where it’s defined — a plain callback loses the surroundingthisentirely, which is exactly what an arrow function callback avoids by having nothisof its own.