DNA⚡ JavaScriptPrototypes & The JavaScript Object Model
ðŸĢHatchlingJavaScriptOOPFundamentals

Prototypes & The JavaScript Object Model

JavaScript doesn't have classical inheritance — it has prototypal delegation. Understanding this distinction is what makes the difference between using the language and understanding it.

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

  1. Check the object itself (own properties)
  2. Check [[Prototype]]
  3. Check [[Prototype]]'s [[Prototype]]
  4. Continue until null (end of chain)
  5. Return undefined if 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
}
  1. Create empty object with [[Prototype]] = Constructor.prototype
  2. Execute constructor with this bound to the new object
  3. If constructor returns an object, use that instead
  4. 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:

  • Animal is a function
  • speak is on Animal.prototype
  • create is a property of Animal itself (not prototype)
  • constructor is 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.prototype

The 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 enumerable

Getters 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:

  1. Delegation, not copying — Properties are looked up at runtime, not copied at creation
  2. The new algorithm — Can explain or implement what new does
  3. class as sugar — Understanding what class/extends compile to under the hood
  4. Mutation vs shadowing — The shared reference trap with prototype objects
  5. Composition preference — Knowing when mixins/composition serve better than deep inheritance