Prototypes & The JavaScript Object Model
JavaScript's object model is fundamentally different from Java, C++, or Python. There are no classes in the traditional sense â class syntax is sugar over a prototypal delegation system. Senior engineers who understand this distinction debug faster, write more idiomatic code, and avoid the traps that catch everyone else.
The Prototype Chain
Every JavaScript object has an internal [[Prototype]] link pointing to another object (or null). Property access walks this chain:
const animal = { eats: true, sleeps: true };
const dog = Object.create(animal);
dog.barks = true;
dog.barks; // true (own property)
dog.eats; // true (found on animal via [[Prototype]])
dog.toString(); // found on Object.prototype
dog.fly; // undefined (not found anywhere in chain)dog â animal â Object.prototype â null
barks eats toString()
sleeps hasOwnProperty()
valueOf()How Property Lookup Works
- Check the object itself (own properties)
- Check
[[Prototype]] - Check
[[Prototype]]'s[[Prototype]] - Continue until
null(end of chain) - Return
undefinedif not found
This is delegation, not copying â dog doesn't have a copy of eats. It delegates the lookup to animal at runtime.
Object.create â The Pure Prototypal Pattern
const eventEmitter = {
_listeners: null,
on(event, fn) {
if (!this._listeners) this._listeners = {};
(this._listeners[event] ??= []).push(fn);
},
emit(event, ...args) {
(this._listeners?.[event] ?? []).forEach(fn => fn(...args));
},
};
const myEmitter = Object.create(eventEmitter);
myEmitter.on('data', (d) => console.log(d));
myEmitter.emit('data', 'hello'); // "hello"Object.create(proto) creates a new object with [[Prototype]] set to proto. No constructor, no new, no ceremony.
Constructor Functions
Before class, this was the standard pattern:
function User(name, role) {
this.name = name;
this.role = role;
}
User.prototype.greet = function() {
return `${this.name} (${this.role})`;
};
const admin = new User('Alice', 'admin');What new Does (4 Steps)
function new_simulation(Constructor, ...args) {
const obj = Object.create(Constructor.prototype); // 1. Create object with prototype
const result = Constructor.apply(obj, args); // 2. Call constructor with this = obj
return result instanceof Object ? result : obj; // 3. Return result if object, else obj
}- Create empty object with
[[Prototype]]=Constructor.prototype - Execute constructor with
thisbound to the new object - If constructor returns an object, use that instead
- Otherwise, return the new object
class Is Syntactic Sugar
class Animal {
constructor(name) {
this.name = name;
}
speak() {
return `${this.name} makes a noise`;
}
static create(name) {
return new Animal(name);
}
}This compiles to:
Animalis a functionspeakis onAnimal.prototypecreateis a property ofAnimalitself (not prototype)constructoris the function body
extends Sets Up Two Prototype Chains
class Dog extends Animal {
bark() {
return `${this.name} barks`;
}
}Instance chain: dog â Dog.prototype â Animal.prototype â Object.prototype â null
Static chain: Dog â Animal â Function.prototypeThe static chain means Dog.create works (inherited static method).
super â Not What You Think
class Child extends Parent {
constructor(x) {
super(x); // Must call before using `this`
this.extra = true;
}
method() {
super.method(); // Calls Parent.prototype.method
}
}super() in constructor calls the parent constructor with the child's this. super.method() does a prototype lookup starting from Parent.prototype, not from this.
Property Descriptors
Every property has hidden attributes:
const obj = {};
Object.defineProperty(obj, 'id', {
value: 42,
writable: false, // Can't reassign
enumerable: false, // Won't show in for...in or Object.keys
configurable: false, // Can't delete or reconfigure
});
obj.id = 100; // Silently fails (strict mode: TypeError)
Object.keys(obj); // [] â not enumerableGetters and Setters
const user = {
_name: '',
get name() {
return this._name.toUpperCase();
},
set name(value) {
if (typeof value !== 'string') throw new TypeError('Name must be string');
this._name = value.trim();
},
};
user.name = ' alice ';
user.name; // "ALICE"Prototype Gotchas
Mutation vs Shadowing
const parent = { items: [1, 2, 3] };
const child = Object.create(parent);
child.items.push(4); // Mutates parent's array!
parent.items; // [1, 2, 3, 4]
child.items = [10, 20]; // Creates own property (shadowing)
parent.items; // [1, 2, 3, 4] (unchanged now)
child.items; // [10, 20]Reading child.items delegates to parent (returns the same reference). Pushing mutates that shared reference. Assignment creates a new own property.
instanceof Walks the Chain
const fakeArray = Object.create(Array.prototype);
fakeArray instanceof Array; // true (prototype chain matches)
Array.isArray(fakeArray); // false (not a real array)hasOwnProperty vs in
const obj = Object.create({ inherited: true });
obj.own = true;
obj.hasOwnProperty('own'); // true
obj.hasOwnProperty('inherited'); // false
'own' in obj; // true
'inherited' in obj; // true (walks chain)Patterns: Composition Over Inheritance
JavaScript's prototype system makes composition natural:
const withLogging = (base) => ({
...base,
log(msg) { console.log(`[${this.name}] ${msg}`); },
});
const withValidation = (base) => ({
...base,
validate() { return Object.keys(this).every(k => this[k] != null); },
});
const createUser = (name) =>
withValidation(withLogging({
name,
email: null,
}));Object.assign for Mixins
const Serializable = {
toJSON() { return JSON.stringify(this); },
fromJSON(json) { return Object.assign(Object.create(this), JSON.parse(json)); },
};
const Timestamped = {
touch() { this.updatedAt = Date.now(); },
};
function createModel(data) {
return Object.assign(Object.create(null), Serializable, Timestamped, data);
}Interview Signal
Senior candidates demonstrate:
- Delegation, not copying â Properties are looked up at runtime, not copied at creation
- The
newalgorithm â Can explain or implement whatnewdoes - class as sugar â Understanding what class/extends compile to under the hood
- Mutation vs shadowing â The shared reference trap with prototype objects
- Composition preference â Knowing when mixins/composition serve better than deep inheritance