Objects All the Way Down
JavaScript is fundamentally an object-based language. Unlike class-based languages (Java, C#), JavaScript uses prototypal inheritance โ objects inherit directly from other objects. There are no classes at the engine level (ES6 class is syntactic sugar). Understanding prototypes is understanding how JavaScript really works.
Every object in JavaScript has a hidden internal link to another object called its prototype. When you access a property on an object and it's not found, JavaScript follows this link to the prototype, then the prototype's prototype, and so on. This is the prototype chain.
โโโโโโโโโโโโโโโโโโโโโโ
โ myObj โ
โ name: 'Alice' โ
โ [[Prototype]] โโโโผโโโบ โโโโโโโโโโโโโโโโโโโโโโ
โโโโโโโโโโโโโโโโโโโโโโ โ personProto โ
โ greet: function โ
โ [[Prototype]] โโโโโผโโโบ โโโโโโโโโโโโโโโโโโโโ
โโโโโโโโโโโโโโโโโโโโโโโ โ Object.prototype โ
โ toString() โ
โ hasOwnProperty โ
โ [[Prototype]] โโผโโโบ null
โโโโโโโโโโโโโโโโโโโโ
When you call myObj.greet(), JS looks:
- On
myObjโ not found - On
personProto(myObj's prototype) โ found, execute it - If not there either, continues to
Object.prototype, thennull(end)
proto vs prototype vs [[Prototype]]
Three related but different concepts that cause massive confusion. Let's clarify:
โโโโโโโโโโโโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Term โ What It Is โ
โโโโโโโโโโโโโโโโโโโโโโโโโโผโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค
โ [[Prototype]] โ The actual internal link (spec-level, hidden) โ
โ __proto__ โ Accessor property to read/write [[Prototype]] โ
โ Function.prototype โ Object assigned as [[Prototype]] of instances โ
โ โ created with `new Function()` โ
โโโโโโโโโโโโโโโโโโโโโโโโโโดโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
function Dog(name) {
this.name = name;
}
Dog.prototype.bark = function () {
return `${this.name} says woof!`;
};
const rex = new Dog('Rex');
// These all refer to the same object:
rex.__proto__ === Dog.prototype; // true
Object.getPrototypeOf(rex) === Dog.prototype; // true
// Dog.prototype is NOT the prototype OF Dog
// It's the prototype FOR instances created with new Dog()
Dog.__proto__ === Function.prototype; // true (Dog is a function)
Dog (function)
โโโ .prototype โโโบ Dog.prototype { bark: fn, constructor: Dog }
โ โ
โ โโโ [[Prototype]] โโโบ Object.prototype โโโบ null
โ
โโโ [[Prototype]] โโโบ Function.prototype โโโบ Object.prototype โโโบ null
rex (instance)
โโโ [[Prototype]] โโโบ Dog.prototype { bark: fn }
Best practice: Use Object.getPrototypeOf(obj) instead of obj.__proto__. The latter is deprecated (though still supported everywhere).
Constructor Functions
Before ES6 classes, constructor functions were the standard way to create objects with shared behavior.
function Person(name, age) {
// 'this' refers to the new object being created
this.name = name;
this.age = age;
}
// Methods go on the prototype โ shared by all instances
Person.prototype.greet = function () {
return `Hi, I'm ${this.name} and I'm ${this.age}`;
};
Person.prototype.birthday = function () {
this.age++;
return this.age;
};
const alice = new Person('Alice', 30);
const bob = new Person('Bob', 25);
alice.greet(); // "Hi, I'm Alice and I'm 30"
bob.greet(); // "Hi, I'm Bob and I'm 25"
// Both share the same function reference
alice.greet === bob.greet; // true โ same function on prototype
What new Does
When you call new Person('Alice', 30), four things happen:
// new Person('Alice', 30) is roughly equivalent to:
// 1. Create a new empty object
const obj = {};
// 2. Set its [[Prototype]] to Person.prototype
Object.setPrototypeOf(obj, Person.prototype);
// 3. Call Person with 'this' bound to the new object
const result = Person.call(obj, 'Alice', 30);
// 4. Return the object (unless Person returned an object)
return result instanceof Object ? result : obj;
Why Methods Go on the Prototype
If methods were inside the constructor, every instance would get its own copy:
// BAD โ each instance gets a separate copy of greet
function Person(name) {
this.name = name;
this.greet = function () {
return `Hi, I'm ${this.name}`;
};
}
const a = new Person('A');
const b = new Person('B');
a.greet === b.greet; // false โ two different function objects
// GOOD โ single copy shared via prototype
function Person(name) {
this.name = name;
}
Person.prototype.greet = function () {
return `Hi, I'm ${this.name}`;
};
const c = new Person('C');
const d = new Person('D');
c.greet === d.greet; // true โ same function object
For 1000 instances, the bad pattern creates 1000 function objects. The prototype pattern creates 1.
Object.create
Object.create creates a new object with a specified prototype. It's the most direct way to set up prototypal inheritance.
const animal = {
type: 'Animal',
describe() {
return `${this.name} is a ${this.type}`;
},
};
const dog = Object.create(animal);
dog.name = 'Rex';
dog.type = 'Dog';
dog.bark = function () {
return `${this.name} says woof!`;
};
dog.describe(); // "Rex is a Dog" โ found on animal prototype
dog.bark(); // "Rex says woof!" โ found on dog itself
Object.create(null) โ No Prototype
Creates a truly empty object with no prototype chain:
const dict = Object.create(null);
dict.toString; // undefined โ no Object.prototype
dict.hasOwnProperty; // undefined
// Useful for pure dictionaries/maps
dict['key'] = 'value';
// No prototype pollution risk
'toString' in dict; // false
Multi-Level Prototype Chain
const living = {
isAlive: true,
breathe() {
return 'breathing...';
},
};
const animal = Object.create(living);
animal.eat = function () {
return 'eating...';
};
const dog = Object.create(animal);
dog.bark = function () {
return 'woof!';
};
const rex = Object.create(dog);
rex.name = 'Rex';
rex.bark(); // 'woof!' โ from dog
rex.eat(); // 'eating...' โ from animal
rex.breathe(); // 'breathing...' โ from living
rex.isAlive; // true โ from living
rex.name; // 'Rex' โ own property
rex
โโโ [[Prototype]] โโโบ dog { bark }
โโโ [[Prototype]] โโโบ animal { eat }
โโโ [[Prototype]] โโโบ living { isAlive, breathe }
โโโ [[Prototype]] โโโบ Object.prototype
โโโ [[Prototype]] โโโบ null
ES6 Classes
ES6 class syntax is syntactic sugar over prototype-based inheritance. Under the hood, it does the same thing as constructor functions + prototypes.
class Person {
constructor(name, age) {
this.name = name;
this.age = age;
}
greet() {
return `Hi, I'm ${this.name}`;
}
birthday() {
this.age++;
return this.age;
}
}
const alice = new Person('Alice', 30);
alice.greet(); // "Hi, I'm Alice"
// Under the hood:
typeof Person; // 'function'
alice.__proto__ === Person.prototype; // true
Person.prototype.greet === alice.greet; // true
Static Methods and Properties
class MathUtils {
static PI = 3.14159265;
static add(a, b) {
return a + b;
}
static random(min, max) {
return Math.floor(Math.random() * (max - min + 1)) + min;
}
}
MathUtils.add(2, 3); // 5
MathUtils.PI; // 3.14159265
// Static methods exist on the class itself, NOT on instances
const m = new MathUtils();
m.add; // undefined
Private Fields and Methods (ES2022)
True private members using the # prefix:
class BankAccount {
#balance;
#pin;
constructor(initialBalance, pin) {
this.#balance = initialBalance;
this.#pin = pin;
}
#validatePin(pin) {
return pin === this.#pin;
}
deposit(amount) {
if (amount <= 0) throw new Error('Invalid amount');
this.#balance += amount;
return this.#balance;
}
withdraw(amount, pin) {
if (!this.#validatePin(pin)) throw new Error('Invalid PIN');
if (amount > this.#balance) throw new Error('Insufficient funds');
this.#balance -= amount;
return this.#balance;
}
get balance() {
return this.#balance;
}
}
const account = new BankAccount(1000, '1234');
account.balance; // 1000 (getter)
account.deposit(500); // 1500
account.withdraw(200, '1234'); // 1300
// account.#balance; // SyntaxError: Private field
// account.#validatePin; // SyntaxError: Private field
Getters and Setters
class Temperature {
#celsius;
constructor(celsius) {
this.#celsius = celsius;
}
get fahrenheit() {
return this.#celsius * 9 / 5 + 32;
}
set fahrenheit(f) {
this.#celsius = (f - 32) * 5 / 9;
}
get celsius() {
return this.#celsius;
}
set celsius(c) {
if (c < -273.15) throw new Error('Below absolute zero');
this.#celsius = c;
}
}
const temp = new Temperature(100);
temp.fahrenheit; // 212
temp.fahrenheit = 32;
temp.celsius; // 0
Inheritance Patterns
Constructor Function Inheritance (Pre-ES6)
function Animal(name) {
this.name = name;
}
Animal.prototype.speak = function () {
return `${this.name} makes a sound`;
};
function Dog(name, breed) {
Animal.call(this, name); // call parent constructor
this.breed = breed;
}
// Set up inheritance
Dog.prototype = Object.create(Animal.prototype);
Dog.prototype.constructor = Dog; // fix constructor reference
Dog.prototype.bark = function () {
return `${this.name} barks!`;
};
const rex = new Dog('Rex', 'Shepherd');
rex.speak(); // "Rex makes a sound" โ inherited from Animal
rex.bark(); // "Rex barks!" โ own method
rex instanceof Dog; // true
rex instanceof Animal; // true
rex
โโโ [[Prototype]] โโโบ Dog.prototype { bark, constructor: Dog }
โโโ [[Prototype]] โโโบ Animal.prototype { speak, constructor: Animal }
โโโ [[Prototype]] โโโบ Object.prototype
โโโ [[Prototype]] โโโบ null
ES6 Class Inheritance
Much cleaner syntax with extends and super:
class Animal {
constructor(name) {
this.name = name;
}
speak() {
return `${this.name} makes a sound`;
}
}
class Dog extends Animal {
constructor(name, breed) {
super(name); // MUST call super() before using 'this'
this.breed = breed;
}
bark() {
return `${this.name} barks!`;
}
// Override parent method
speak() {
return `${this.name} barks loudly!`;
}
}
class GuideDog extends Dog {
constructor(name, breed, owner) {
super(name, breed);
this.owner = owner;
}
assist() {
return `${this.name} guides ${this.owner}`;
}
// Call parent's original speak
speak() {
return `${super.speak()} while guiding ${this.owner}`;
}
}
const buddy = new GuideDog('Buddy', 'Labrador', 'Alice');
buddy.speak(); // "Buddy barks loudly! while guiding Alice"
buddy.bark(); // "Buddy barks!"
buddy.assist(); // "Buddy guides Alice"
Mixin Pattern โ Multiple Inheritance Workaround
JavaScript doesn't support multiple inheritance, but you can compose behavior with mixins:
const Serializable = (Base) =>
class extends Base {
serialize() {
return JSON.stringify(this);
}
static deserialize(json) {
return Object.assign(new this(), JSON.parse(json));
}
};
const Validatable = (Base) =>
class extends Base {
validate() {
for (const [key, rules] of Object.entries(this.constructor.rules || {})) {
for (const rule of rules) {
if (!rule.test(this[key])) {
throw new Error(`${key}: ${rule.message}`);
}
}
}
return true;
}
};
const Timestamped = (Base) =>
class extends Base {
constructor(...args) {
super(...args);
this.createdAt = Date.now();
this.updatedAt = Date.now();
}
touch() {
this.updatedAt = Date.now();
}
};
// Compose mixins
class User extends Serializable(Validatable(Timestamped(class {}))) {
static rules = {
name: [{ test: (v) => v?.length > 0, message: 'Name required' }],
email: [{ test: (v) => v?.includes('@'), message: 'Invalid email' }],
};
constructor(name, email) {
super();
this.name = name;
this.email = email;
}
}
const user = new User('Alice', '[email protected]');
user.validate(); // true
user.serialize(); // '{"createdAt":...,"updatedAt":...,"name":"Alice","email":"[email protected]"}'
user.createdAt; // timestamp
Composition Over Inheritance
Modern JS tends to prefer composition. Instead of deep inheritance chains, compose behavior:
// Instead of: class FlyingSwimmingAnimal extends SwimmingAnimal extends Animal
// Use composition:
function withSwimming(obj) {
return {
...obj,
swim() {
return `${obj.name} is swimming`;
},
};
}
function withFlying(obj) {
return {
...obj,
fly() {
return `${obj.name} is flying`;
},
};
}
function withSpeaking(obj) {
return {
...obj,
speak(sound) {
return `${obj.name} says ${sound}`;
},
};
}
function createDuck(name) {
const base = { name };
return withSpeaking(withSwimming(withFlying(base)));
}
const donald = createDuck('Donald');
donald.swim(); // "Donald is swimming"
donald.fly(); // "Donald is flying"
donald.speak('quack'); // "Donald says quack"
Type Checking โ instanceof, typeof, and More
typeof
Returns a string indicating the type. Limited for objects.
typeof 42; // 'number'
typeof 'hello'; // 'string'
typeof true; // 'boolean'
typeof undefined; // 'undefined'
typeof null; // 'object' โ famous bug, never fixed
typeof {}; // 'object'
typeof []; // 'object' โ arrays are objects
typeof function(){}; // 'function'
typeof Symbol(); // 'symbol'
typeof 10n; // 'bigint'
instanceof
Checks if an object's prototype chain includes a constructor's prototype:
class Animal {}
class Dog extends Animal {}
class Cat extends Animal {}
const rex = new Dog();
rex instanceof Dog; // true
rex instanceof Animal; // true โ Dog extends Animal
rex instanceof Cat; // false
rex instanceof Object; // true โ everything extends Object
// How instanceof works internally:
function myInstanceOf(obj, Constructor) {
let proto = Object.getPrototypeOf(obj);
while (proto !== null) {
if (proto === Constructor.prototype) return true;
proto = Object.getPrototypeOf(proto);
}
return false;
}
Object.keys, Object.values, Object.entries
These only return own enumerable properties (not inherited ones):
const parent = { inherited: true };
const child = Object.create(parent);
child.own = 'yes';
child.also = 'mine';
Object.keys(child); // ['own', 'also'] โ no 'inherited'
Object.values(child); // ['yes', 'mine']
Object.entries(child); // [['own', 'yes'], ['also', 'mine']]
// To check for inherited properties:
'inherited' in child; // true โ checks whole chain
child.hasOwnProperty('inherited'); // false โ own only
child.hasOwnProperty('own'); // true
Checking the Full Prototype Chain
function getPrototypeChain(obj) {
const chain = [];
let current = Object.getPrototypeOf(obj);
while (current !== null) {
chain.push(current);
current = Object.getPrototypeOf(current);
}
return chain;
}
class A {}
class B extends A {}
class C extends B {}
const c = new C();
getPrototypeChain(c);
// [C.prototype, B.prototype, A.prototype, Object.prototype]
Property Lookup and Shadowing
When you access a property, JS walks the prototype chain. When you set a property, it always goes on the object itself (creating a "shadow"):
const parent = {
x: 10,
greet() {
return 'hello from parent';
},
};
const child = Object.create(parent);
// Reading โ walks the chain
child.x; // 10 (found on parent)
child.greet(); // 'hello from parent' (found on parent)
// Writing โ creates own property (shadow)
child.x = 20;
child.x; // 20 (own property, shadows parent)
parent.x; // 10 (unchanged)
child.hasOwnProperty('x'); // true โ now has its own
Before child.x = 20: After child.x = 20:
child { } child { x: 20 } โ shadow
โโโ parent { x: 10 } โโโ parent { x: 10 } โ still 10
child.x โ 10 (from parent) child.x โ 20 (own property)
Gotcha: Shadowing with Methods
const base = {
count: 0,
increment() {
this.count++; // 'this' is the calling object, not base
},
};
const obj = Object.create(base);
obj.increment();
obj.count; // 1 โ created as own property on obj
base.count; // 0 โ base is untouched
// this.count++ is equivalent to:
// this.count = this.count + 1;
// Reading this.count โ finds 0 on base (prototype chain)
// Writing this.count โ creates own property on obj
Performance Considerations
Prototype Chain Length
Longer chains mean more lookups. Keep inheritance shallow:
// Deep chain (avoid) โ each property lookup traverses 5 levels
class A {}
class B extends A {}
class C extends B {}
class D extends C {}
class E extends D {}
// Shallow (preferred) โ max 2 levels
class Base {}
class Widget extends Base {}
hasOwnProperty in Hot Paths
If you're iterating frequently, checking hasOwnProperty adds overhead. Use Object.keys() or Object.entries() which only return own properties:
const obj = Object.create({ inherited: true });
obj.a = 1;
obj.b = 2;
// Slow in hot loop โ checks hasOwnProperty each iteration
for (const key in obj) {
if (obj.hasOwnProperty(key)) {
process(obj[key]);
}
}
// Faster โ Object.keys returns only own enumerable properties
for (const key of Object.keys(obj)) {
process(obj[key]);
}
Modifying Built-in Prototypes (Don't)
You can add methods to Array.prototype, String.prototype, etc. You absolutely should not:
// DO NOT DO THIS
Array.prototype.last = function () {
return this[this.length - 1];
};
[1, 2, 3].last(); // 3 โ works, but...
// Problems:
// 1. Conflicts with future language features (Array.prototype.at)
// 2. Breaks for...in loops on arrays
// 3. Conflicts with other libraries doing the same thing
// 4. Makes debugging nightmarish
Object.freeze and Prototypes
Object.freeze only freezes own properties. The prototype is still mutable:
const proto = { shared: 'mutable' };
const obj = Object.create(proto);
obj.own = 'frozen';
Object.freeze(obj);
obj.own = 'changed'; // silently fails (or throws in strict mode)
obj.own; // 'frozen' โ frozen
proto.shared = 'changed';
obj.shared; // 'changed' โ prototype wasn't frozen
Common Patterns Summary
โโโโโโโโโโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Pattern โ Pros โ Cons โ
โโโโโโโโโโโโโโโโโโโโโโโโผโโโโโโโโโโโโโโโโโโโโโโโโโผโโโโโโโโโโโโโโโโโโโโโโโโโโค
โ Constructor โ Familiar, instanceof โ Verbose, manual proto โ
โ Functions โ works โ chain setup โ
โโโโโโโโโโโโโโโโโโโโโโโโผโโโโโโโโโโโโโโโโโโโโโโโโโผโโโโโโโโโโโโโโโโโโโโโโโโโโค
โ ES6 Classes โ Clean syntax, extends, โ Sugar over prototypes, โ
โ โ super, static, private โ can mislead about how โ
โ โ โ JS actually works โ
โโโโโโโโโโโโโโโโโโโโโโโโผโโโโโโโโโโโโโโโโโโโโโโโโโผโโโโโโโโโโโโโโโโโโโโโโโโโโค
โ Object.create โ Direct prototype link, โ No constructor, โ
โ โ simple, explicit โ verbose for many props โ
โโโโโโโโโโโโโโโโโโโโโโโโผโโโโโโโโโโโโโโโโโโโโโโโโโผโโโโโโโโโโโโโโโโโโโโโโโโโโค
โ Factory Functions โ No 'new', no 'this', โ No instanceof, โ
โ (closures) โ true privacy โ memory per instance โ
โโโโโโโโโโโโโโโโโโโโโโโโผโโโโโโโโโโโโโโโโโโโโโโโโโผโโโโโโโโโโโโโโโโโโโโโโโโโโค
โ Mixins โ Compose multiple โ Can be complex, โ
โ โ behaviors โ name collisions โ
โโโโโโโโโโโโโโโโโโโโโโโโผโโโโโโโโโโโโโโโโโโโโโโโโโผโโโโโโโโโโโโโโโโโโโโโโโโโโค
โ Composition โ Flexible, no deep โ No instanceof, โ
โ โ hierarchies โ more verbose โ
โโโโโโโโโโโโโโโโโโโโโโโโดโโโโโโโโโโโโโโโโโโโโโโโโโดโโโโโโโโโโโโโโโโโโโโโโโโโโ
Practical Example: Plugin System
A real-world example combining prototypes with extensibility:
class EventEmitter {
#listeners = new Map();
on(event, callback) {
if (!this.#listeners.has(event)) {
this.#listeners.set(event, []);
}
this.#listeners.get(event).push(callback);
return this;
}
emit(event, ...args) {
const callbacks = this.#listeners.get(event) || [];
for (const cb of callbacks) {
cb.apply(this, args);
}
return this;
}
off(event, callback) {
const callbacks = this.#listeners.get(event);
if (callbacks) {
this.#listeners.set(
event,
callbacks.filter((cb) => cb !== callback)
);
}
return this;
}
}
class PluginSystem extends EventEmitter {
#plugins = new Map();
register(name, plugin) {
if (this.#plugins.has(name)) {
throw new Error(`Plugin "${name}" already registered`);
}
// Validate plugin has required interface
if (typeof plugin.init !== 'function') {
throw new Error(`Plugin "${name}" must have init() method`);
}
this.#plugins.set(name, plugin);
plugin.init(this);
this.emit('plugin:registered', { name, plugin });
return this;
}
getPlugin(name) {
return this.#plugins.get(name);
}
}
// Create a plugin
const loggingPlugin = {
init(system) {
system.on('plugin:registered', ({ name }) => {
console.log(`[Logger] Plugin registered: ${name}`);
});
},
log(message) {
console.log(`[${new Date().toISOString()}] ${message}`);
},
};
const app = new PluginSystem();
app.register('logger', loggingPlugin);
app.getPlugin('logger').log('System started');
Key Takeaways
- Every object has a [[Prototype]] โ the engine follows it to find inherited properties
prototypeis a property on functions โ used as the [[Prototype]] for instances created withnew- Use
Object.getPrototypeOf()โ not__proto__(deprecated) - ES6 classes are sugar โ they compile to constructor functions + prototypes
- Properties shadow, not override โ setting a property on a child creates an own property, parent unchanged
- Keep chains shallow โ deep inheritance hierarchies hurt readability and performance
- Prefer composition over inheritance โ compose small behaviors instead of deep class trees
- Don't modify built-in prototypes โ it causes conflicts and hard-to-debug issues
- Object.create(null) for dictionaries โ no prototype chain means no inherited property surprises
- Understanding prototypes is non-negotiable โ even if you use classes, the prototype system is what runs underneath