Prototypes and Inheritance System

1. Prototype Chain and Property Lookup

Concept Description Key Points
Prototype Internal reference ([[Prototype]]) that links object to another object Forms inheritance chain, enables property sharing
Prototype Chain Series of prototype links from object to Object.prototype to null Property lookup traverses chain until found or reaches null
__proto__ Accessor property to get/set [[Prototype]] (legacy, avoid) Non-standard but widely supported; use Object.getPrototypeOf() instead
Object.prototype Top of prototype chain for normal objects; contains toString, hasOwnProperty, etc. Inherits from null; all objects inherit from it unless explicitly set
Function.prototype Prototype for all functions; contains call, apply, bind, etc. Functions inherit from Function.prototype which inherits from Object.prototype
.prototype Property Property on constructor functions; used as prototype for instances created with new Only constructor functions have .prototype; instances have [[Prototype]]
Property Lookup Step Location Action
1. Own Properties Object itself Check if property exists directly on object; return if found
2. Prototype Object's [[Prototype]] If not found, check prototype; return if found
3. Prototype Chain Prototype's prototype, etc. Continue up chain until property found
4. Object.prototype Top of chain Check Object.prototype; return if found
5. null End of chain Return undefined if not found anywhere

Example: Understanding the prototype chain

// Every object has internal [[Prototype]] link
const obj = { a: 1 };

// Access prototype using Object.getPrototypeOf()
console.log(Object.getPrototypeOf(obj) === Object.prototype);  // true

// Object.prototype's prototype is null (end of chain)
console.log(Object.getPrototypeOf(Object.prototype));  // null

// Property lookup demonstration
const animal = {
    eats: true,
    walk() {
        console.log('Animal walks');
    }
};

const rabbit = {
    jumps: true
};

// Set rabbit's prototype to animal
Object.setPrototypeOf(rabbit, animal);

// rabbit can access both own and inherited properties
console.log(rabbit.jumps);  // true (own property)
console.log(rabbit.eats);   // true (inherited from animal)
rabbit.walk();  // "Animal walks" (inherited method)

// Property lookup order
console.log(rabbit.toString);  // [Function: toString] (from Object.prototype)

// Chain: rabbit -> animal -> Object.prototype -> null
console.log(Object.getPrototypeOf(rabbit) === animal);  // true
console.log(Object.getPrototypeOf(animal) === Object.prototype);  // true
console.log(Object.getPrototypeOf(Object.prototype));  // null

Example: Property shadowing and prototype lookup

const parent = {
    name: 'Parent',
    greet() {
        return `Hello from ${this.name}`;
    }
};

const child = Object.create(parent);
child.age = 10;

// Access inherited property
console.log(child.name);  // 'Parent' (from prototype)
console.log(child.greet());  // "Hello from Parent"

// Own property shadows prototype property
child.name = 'Child';
console.log(child.name);  // 'Child' (own property, shadows prototype)

// Check property ownership
console.log(child.hasOwnProperty('name'));  // true (own)
console.log(child.hasOwnProperty('greet'));  // false (inherited)
console.log('greet' in child);  // true (checks own + prototype)

// Delete own property - prototype property revealed
delete child.name;
console.log(child.name);  // 'Parent' (prototype property no longer shadowed)

// Writing to inherited property creates own property
const obj = Object.create({ x: 1 });
console.log(obj.x);  // 1 (inherited)
obj.x = 2;  // Creates own property (doesn't modify prototype)
console.log(obj.x);  // 2 (own property)
console.log(Object.getPrototypeOf(obj).x);  // 1 (prototype unchanged)

Example: Working with prototypes

// Get prototype of object
const obj = {};
const proto = Object.getPrototypeOf(obj);
console.log(proto === Object.prototype);  // true

// Set prototype (avoid in production - slow!)
const animal = { type: 'animal' };
const dog = { breed: 'Labrador' };
Object.setPrototypeOf(dog, animal);
console.log(dog.type);  // 'animal' (inherited)

// Create object with specific prototype (preferred)
const cat = Object.create(animal);
cat.breed = 'Persian';
console.log(cat.type);  // 'animal' (inherited)

// Check if object is in prototype chain
console.log(animal.isPrototypeOf(dog));  // true
console.log(animal.isPrototypeOf(cat));  // true
console.log(Object.prototype.isPrototypeOf(dog));  // true (all objects)

// Create object with null prototype (no inheritance)
const noProto = Object.create(null);
console.log(noProto.toString);  // undefined (no Object.prototype)
console.log(Object.getPrototypeOf(noProto));  // null

// Prototype chain visualization
function showChain(obj, label = 'obj') {
    console.log(`${label}:`, obj);
    const proto = Object.getPrototypeOf(obj);
    if (proto) {
        showChain(proto, `${label}.__proto__`);
    } else {
        console.log('End of chain: null');
    }
}

const myObj = { x: 1 };
showChain(myObj);
// myObj: { x: 1 }
// myObj.__proto__: Object.prototype
// End of chain: null
Important: Property lookup is read-only up the chain. Writing to a property always creates/modifies own property (unless setter exists in prototype). Use Object.getPrototypeOf() and Object.create() instead of deprecated __proto__. Avoid Object.setPrototypeOf() for performance reasons.

2. Constructor Functions and new Operator

Concept Description Behavior
Constructor Function Regular function used with new to create instances Conventionally capitalized; sets up instance properties
new Operator Creates instance, sets prototype, binds this, returns object 4-step process: create, link, execute, return
Constructor.prototype Object that becomes prototype of instances created with new Constructor() Shared methods placed here for memory efficiency
constructor Property Property on prototype pointing back to constructor function Allows instances to identify their constructor
Instance Properties Properties unique to each instance (set in constructor) Defined using this.property = value
Prototype Methods Methods shared across all instances (on Constructor.prototype) Memory efficient - one copy shared by all instances
Step new Operator Process Effect
1. Create New empty object created const obj = {}
2. Link Prototype Object's [[Prototype]] set to Constructor.prototype Object.setPrototypeOf(obj, Constructor.prototype)
3. Execute Constructor Constructor called with this bound to new object Constructor.call(obj, ...args)
4. Return Return object (or constructor's return if object) Returns this unless constructor explicitly returns object

Example: Constructor functions and instances

// Constructor function (capitalized by convention)
function Person(name, age) {
    // Instance properties (unique per instance)
    this.name = name;
    this.age = age;
    
    // ✗ Avoid methods here (creates new function per instance)
    // this.greet = function() { return `Hi, I'm ${this.name}`; };
}

// Shared methods on prototype (memory efficient)
Person.prototype.greet = function() {
    return `Hello, I'm ${this.name}`;
};

Person.prototype.getAge = function() {
    return this.age;
};

// Create instances with 'new'
const alice = new Person('Alice', 30);
const bob = new Person('Bob', 25);

// Each instance has own properties
console.log(alice.name);  // 'Alice'
console.log(bob.name);  // 'Bob'

// Shared methods from prototype
console.log(alice.greet());  // "Hello, I'm Alice"
console.log(bob.greet());  // "Hello, I'm Bob"

// Methods are shared (same function object)
console.log(alice.greet === bob.greet);  // true (memory efficient)

// Verify prototype relationship
console.log(Object.getPrototypeOf(alice) === Person.prototype);  // true
console.log(alice.constructor === Person);  // true
console.log(alice instanceof Person);  // true

// Prototype chain: alice -> Person.prototype -> Object.prototype -> null
console.log(Object.getPrototypeOf(Person.prototype) === Object.prototype);  // true

Example: Manual simulation of 'new' operator

// Understanding what 'new' does
function myNew(Constructor, ...args) {
    // 1. Create new object
    const obj = {};
    
    // 2. Link to prototype
    Object.setPrototypeOf(obj, Constructor.prototype);
    // Or: obj.__proto__ = Constructor.prototype;
    
    // 3. Execute constructor with 'this' bound to new object
    const result = Constructor.apply(obj, args);
    
    // 4. Return object (or constructor's return if it's an object)
    return (typeof result === 'object' && result !== null) ? result : obj;
}

function Dog(name) {
    this.name = name;
}

Dog.prototype.bark = function() {
    return `${this.name} says woof!`;
};

// Using custom 'new'
const dog1 = myNew(Dog, 'Rex');
console.log(dog1.name);  // 'Rex'
console.log(dog1.bark());  // "Rex says woof!"
console.log(dog1 instanceof Dog);  // true

// Compare with real 'new'
const dog2 = new Dog('Max');
console.log(dog2.name);  // 'Max'
console.log(dog2.bark());  // "Max says woof!"

// Constructor returning object overrides default behavior
function Special() {
    this.x = 1;
    return { y: 2 };  // Explicit object return
}

const s = new Special();
console.log(s.x);  // undefined (returned object used instead)
console.log(s.y);  // 2

// Constructor returning primitive doesn't override
function Normal() {
    this.x = 1;
    return 42;  // Primitive ignored
}

const n = new Normal();
console.log(n.x);  // 1 (this returned as usual)

Example: Constructor patterns and best practices

// Pattern 1: Instance properties + prototype methods (recommended)
function Car(make, model) {
    this.make = make;
    this.model = model;
}

Car.prototype.getDescription = function() {
    return `${this.make} ${this.model}`;
};

// Pattern 2: Static methods (on constructor itself)
Car.fromString = function(str) {
    const [make, model] = str.split(' ');
    return new Car(make, model);
};

const car1 = new Car('Toyota', 'Camry');
const car2 = Car.fromString('Honda Civic');

// Pattern 3: Guard against missing 'new'
function SafeConstructor(value) {
    // Check if called with 'new'
    if (!(this instanceof SafeConstructor)) {
        return new SafeConstructor(value);
    }
    this.value = value;
}

const obj1 = new SafeConstructor(10);  // With 'new'
const obj2 = SafeConstructor(20);  // Without 'new' - still works
console.log(obj1 instanceof SafeConstructor);  // true
console.log(obj2 instanceof SafeConstructor);  // true

// Pattern 4: Setting prototype after constructor definition
function Animal(name) {
    this.name = name;
}

// Replace entire prototype (need to restore constructor)
Animal.prototype = {
    constructor: Animal,  // Restore constructor reference
    speak() {
        return `${this.name} makes a sound`;
    },
    eat() {
        return `${this.name} is eating`;
    }
};

const animal = new Animal('Lion');
console.log(animal.speak());  // "Lion makes a sound"
console.log(animal.constructor === Animal);  // true (restored)

// ✗ Common mistake: adding methods as instance properties
function Inefficient(name) {
    this.name = name;
    // ✗ Bad: creates new function for each instance
    this.greet = function() {
        return `Hi, I'm ${this.name}`;
    };
}

const i1 = new Inefficient('A');
const i2 = new Inefficient('B');
console.log(i1.greet === i2.greet);  // false (different functions - wasteful)
Common Pitfalls:
  • Forgetting 'new': Constructor called as regular function; this refers to global object (or undefined in strict mode)
  • Methods as instance properties: Creating functions inside constructor wastes memory; use prototype instead
  • Overwriting prototype: Breaks constructor reference; always restore .constructor property
  • Modifying built-in prototypes: Never modify Object.prototype, Array.prototype, etc. (causes conflicts)

3. Object.create and Prototypal Inheritance

Method Syntax Creates Use Case
Object.create(proto) Object.create(protoObj) New object with protoObj as prototype Prototypal inheritance, object delegation
Object.create(proto, descriptors) Object.create(proto, { props }) Object with prototype + property descriptors Fine-grained property control
Object.create(null) Object.create(null) Object with no prototype (null) Pure data storage, avoid prototype pollution
Inheritance Pattern Approach Pros Cons
Constructor Pattern function + new + prototype Familiar to class-based developers Verbose, easy to forget 'new'
Object.create Pattern Object.create(proto) Simple, pure prototypal, no 'new' Less familiar, no constructor function
ES6 Class class syntax Clean syntax, modern, standard Syntactic sugar over prototypes
Factory Function function returning object Flexible, private state via closure No shared prototype methods (unless manually added)

Example: Object.create for prototypal inheritance

// Parent object (prototype)
const animal = {
    type: 'animal',
    eat() {
        return `${this.name} is eating`;
    },
    sleep() {
        return `${this.name} is sleeping`;
    }
};

// Create child objects inheriting from parent
const dog = Object.create(animal);
dog.name = 'Rex';
dog.breed = 'Labrador';
dog.bark = function() {
    return `${this.name} says woof!`;
};

const cat = Object.create(animal);
cat.name = 'Whiskers';
cat.breed = 'Persian';
cat.meow = function() {
    return `${this.name} says meow!`;
};

// Access own properties
console.log(dog.name);  // 'Rex' (own)
console.log(dog.breed);  // 'Labrador' (own)

// Access inherited properties/methods
console.log(dog.type);  // 'animal' (inherited)
console.log(dog.eat());  // "Rex is eating" (inherited method)
console.log(dog.bark());  // "Rex says woof!" (own method)

// Verify prototype chain
console.log(Object.getPrototypeOf(dog) === animal);  // true
console.log(Object.getPrototypeOf(cat) === animal);  // true

// Shared prototype
console.log(dog.eat === cat.eat);  // true (same method from prototype)

// Modify prototype affects all children
animal.move = function() {
    return `${this.name} is moving`;
};
console.log(dog.move());  // "Rex is moving" (newly added)
console.log(cat.move());  // "Whiskers is moving"

Example: Object.create with property descriptors

const parent = {
    greet() {
        return `Hello from ${this.name}`;
    }
};

// Create object with prototype and property descriptors
const child = Object.create(parent, {
    name: {
        value: 'Child',
        writable: true,
        enumerable: true,
        configurable: true
    },
    age: {
        value: 10,
        writable: true,
        enumerable: true,
        configurable: true
    },
    // Accessor property
    description: {
        get() {
            return `${this.name}, age ${this.age}`;
        },
        enumerable: true,
        configurable: true
    }
});

console.log(child.name);  // 'Child'
console.log(child.age);  // 10
console.log(child.greet());  // "Hello from Child" (inherited)
console.log(child.description);  // "Child, age 10" (computed)

// Object with null prototype (no inheritance)
const dict = Object.create(null);
dict.key1 = 'value1';
dict.key2 = 'value2';

console.log(dict.toString);  // undefined (no Object.prototype)
console.log(dict.hasOwnProperty);  // undefined
console.log(Object.getPrototypeOf(dict));  // null

// Useful for pure data storage
console.log('toString' in dict);  // false (safe from prototype pollution)

Example: Implementing classical inheritance with Object.create

// Parent constructor
function Animal(name) {
    this.name = name;
}

Animal.prototype.eat = function() {
    return `${this.name} is eating`;
};

Animal.prototype.sleep = function() {
    return `${this.name} is sleeping`;
};

// Child constructor
function Dog(name, breed) {
    // Call parent constructor
    Animal.call(this, name);  // Inherit instance properties
    this.breed = breed;
}

// Set up prototype chain (critical step)
Dog.prototype = Object.create(Animal.prototype);
Dog.prototype.constructor = Dog;  // Restore constructor reference

// Add child-specific methods
Dog.prototype.bark = function() {
    return `${this.name} says woof!`;
};

// Create instance
const dog = new Dog('Rex', 'Labrador');

console.log(dog.name);  // 'Rex' (from Animal)
console.log(dog.breed);  // 'Labrador' (from Dog)
console.log(dog.eat());  // "Rex is eating" (inherited from Animal)
console.log(dog.bark());  // "Rex says woof!" (from Dog)

// Verify prototype chain
console.log(dog instanceof Dog);  // true
console.log(dog instanceof Animal);  // true
console.log(dog.constructor === Dog);  // true

// Chain: dog -> Dog.prototype -> Animal.prototype -> Object.prototype -> null
console.log(Object.getPrototypeOf(dog) === Dog.prototype);  // true
console.log(Object.getPrototypeOf(Dog.prototype) === Animal.prototype);  // true

// ✗ Wrong way (don't do this)
// Dog.prototype = Animal.prototype;  // Shares prototype (modifications affect both)
// Dog.prototype = new Animal();  // Creates unnecessary instance

// ✓ Correct way (ES6+ alternative)
class AnimalClass {
    constructor(name) {
        this.name = name;
    }
    eat() {
        return `${this.name} is eating`;
    }
}

class DogClass extends AnimalClass {
    constructor(name, breed) {
        super(name);
        this.breed = breed;
    }
    bark() {
        return `${this.name} says woof!`;
    }
}

const modernDog = new DogClass('Max', 'Beagle');
console.log(modernDog.eat());  // "Max is eating"
console.log(modernDog.bark());  // "Max says woof!"
Best Practice: For inheritance, use ES6 classes (cleaner syntax). If using constructor functions, always use Object.create(Parent.prototype) to set up prototype chain, not new Parent() or direct assignment. Always restore constructor property. Use Object.create(null) for dictionary objects to avoid prototype pollution.

4. Prototype Methods and Property Override

Concept Description Behavior
Method Inheritance Child inherits methods from parent's prototype Shared method executed with child instance as this
Method Override Child defines method with same name as parent Child method shadows parent method in prototype chain
super Pattern Call parent method from overridden child method Use Parent.prototype.method.call(this) or ES6 super
Property Shadowing Own property hides prototype property with same name Lookup stops at first match (own properties checked first)
Dynamic Dispatch this binding determined at call time, not definition Enables polymorphism - same method name, different behavior
Pattern Implementation Use Case
Simple Override Define method on child with same name Completely replace parent behavior
Extend Parent Method Call parent method + add child logic Augment parent behavior
Delegate to Parent Forward some calls to parent method Conditional behavior based on state
Mixin Pattern Copy methods from multiple sources Multiple inheritance simulation

Example: Method inheritance and override

// Parent with methods
const animal = {
    speak() {
        return `${this.name} makes a sound`;
    },
    eat() {
        return `${this.name} is eating`;
    }
};

// Child inherits and overrides
const dog = Object.create(animal);
dog.name = 'Rex';

// Inherited method (no override)
console.log(dog.eat());  // "Rex is eating" (from animal)

// Override speak method
dog.speak = function() {
    return `${this.name} barks loudly`;
};

console.log(dog.speak());  // "Rex barks loudly" (overridden)

// Access parent method explicitly
console.log(animal.speak.call(dog));  // "Rex makes a sound" (parent method)

// Another child with different override
const cat = Object.create(animal);
cat.name = 'Whiskers';
cat.speak = function() {
    return `${this.name} meows softly`;
};

console.log(cat.speak());  // "Whiskers meows softly" (different override)

// Polymorphism in action
const animals = [dog, cat];
animals.forEach(a => console.log(a.speak()));
// "Rex barks loudly"
// "Whiskers meows softly"

Example: Calling parent methods (super pattern)

// Constructor-based inheritance
function Animal(name) {
    this.name = name;
}

Animal.prototype.describe = function() {
    return `This is ${this.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;
}

Dog.prototype = Object.create(Animal.prototype);
Dog.prototype.constructor = Dog;

// Override with extension - call parent method
Dog.prototype.describe = function() {
    // Call parent method
    const parentDesc = Animal.prototype.describe.call(this);
    // Add child-specific info
    return `${parentDesc}, a ${this.breed}`;
};

Dog.prototype.speak = function() {
    // Completely override (no parent call)
    return `${this.name} barks`;
};

const dog = new Dog('Rex', 'Labrador');
console.log(dog.describe());  // "This is Rex, a Labrador" (extended)
console.log(dog.speak());  // "Rex barks" (overridden)

// ES6 class with super keyword (cleaner)
class AnimalClass {
    constructor(name) {
        this.name = name;
    }
    
    describe() {
        return `This is ${this.name}`;
    }
    
    speak() {
        return `${this.name} makes a sound`;
    }
}

class DogClass extends AnimalClass {
    constructor(name, breed) {
        super(name);  // Call parent constructor
        this.breed = breed;
    }
    
    describe() {
        // Call parent method with super
        return `${super.describe()}, a ${this.breed}`;
    }
    
    speak() {
        // Override without calling parent
        return `${this.name} barks`;
    }
}

const modernDog = new DogClass('Max', 'Beagle');
console.log(modernDog.describe());  // "This is Max, a Beagle"
console.log(modernDog.speak());  // "Max barks"

Example: Property shadowing and resolution

const parent = {
    name: 'Parent',
    value: 10,
    getValue() {
        return this.value;
    }
};

const child = Object.create(parent);
console.log(child.name);  // 'Parent' (inherited)
console.log(child.value);  // 10 (inherited)

// Shadow property with own value
child.name = 'Child';
child.value = 20;

console.log(child.name);  // 'Child' (own property shadows parent)
console.log(child.value);  // 20 (own)
console.log(parent.name);  // 'Parent' (parent unchanged)

// Method uses 'this.value' - gets child's own value
console.log(child.getValue());  // 20 (inherited method, child's value)

// Delete own property - parent property revealed
delete child.name;
console.log(child.name);  // 'Parent' (parent property no longer shadowed)

// hasOwnProperty distinguishes own vs inherited
console.log(child.hasOwnProperty('value'));  // true (own)
console.log(child.hasOwnProperty('getValue'));  // false (inherited)

// Writing to inherited property creates own property
const obj = Object.create({ x: 1, y: 2 });
obj.x = 10;  // Creates own property
console.log(obj.x);  // 10 (own)
console.log(Object.getPrototypeOf(obj).x);  // 1 (prototype unchanged)

// Arrays inherit push, pop, etc. from Array.prototype
const arr = [1, 2, 3];
console.log(arr.hasOwnProperty('push'));  // false (inherited)
console.log(arr.hasOwnProperty('length'));  // true (own property)
console.log(arr.hasOwnProperty(0));  // true (own property)

Example: Mixin pattern for method sharing

// Mixin: reusable method collections
const canEat = {
    eat(food) {
        return `${this.name} is eating ${food}`;
    }
};

const canWalk = {
    walk() {
        return `${this.name} is walking`;
    }
};

const canSwim = {
    swim() {
        return `${this.name} is swimming`;
    }
};

// Mixin helper function
function mixin(target, ...sources) {
    Object.assign(target, ...sources);
    return target;
}

// Create object with multiple mixins
function Animal(name) {
    this.name = name;
}

// Add mixins to prototype
mixin(Animal.prototype, canEat, canWalk);

const dog = new Animal('Rex');
console.log(dog.eat('bone'));  // "Rex is eating bone"
console.log(dog.walk());  // "Rex is walking"
// console.log(dog.swim());  // Error: swim not available

// Different mix of capabilities
function Fish(name) {
    this.name = name;
}

mixin(Fish.prototype, canEat, canSwim);

const fish = new Fish('Nemo');
console.log(fish.eat('algae'));  // "Nemo is eating algae"
console.log(fish.swim());  // "Nemo is swimming"
// console.log(fish.walk());  // Error: walk not available

// ES6 class with mixins
const Flyable = {
    fly() {
        return `${this.name} is flying`;
    }
};

class Bird {
    constructor(name) {
        this.name = name;
    }
}

// Apply mixin to class
Object.assign(Bird.prototype, canEat, Flyable);

const bird = new Bird('Tweety');
console.log(bird.eat('seeds'));  // "Tweety is eating seeds"
console.log(bird.fly());  // "Tweety is flying"
Best Practice: Use method override for polymorphism. Call parent methods with super (ES6 classes) or Parent.prototype.method.call(this) (constructor functions). Use mixins for horizontal code reuse. Remember: methods use this binding, so inherited methods access instance properties correctly.

5. instanceof and Prototype Testing

Method/Operator Syntax Returns What It Checks
instanceof obj instanceof Constructor boolean Is Constructor.prototype in obj's prototype chain?
isPrototypeOf() proto.isPrototypeOf(obj) boolean Is proto in obj's prototype chain?
Object.getPrototypeOf() Object.getPrototypeOf(obj) object or null Returns obj's direct prototype
hasOwnProperty() obj.hasOwnProperty(prop) boolean Is prop an own property (not inherited)?
Object.hasOwn() ES2022 Object.hasOwn(obj, prop) boolean Safer alternative to hasOwnProperty
in operator 'prop' in obj boolean Is prop in obj or its prototype chain?
Test Own Property Inherited Property Nonexistent
obj.prop value value undefined
'prop' in obj true true false
obj.hasOwnProperty('prop') true false false
Object.hasOwn(obj, 'prop') true false false

Example: instanceof operator

// Constructor-based type checking
function Animal(name) {
    this.name = name;
}

function Dog(name, breed) {
    Animal.call(this, name);
    this.breed = breed;
}

Dog.prototype = Object.create(Animal.prototype);
Dog.prototype.constructor = Dog;

const dog = new Dog('Rex', 'Labrador');

// instanceof checks prototype chain
console.log(dog instanceof Dog);  // true
console.log(dog instanceof Animal);  // true
console.log(dog instanceof Object);  // true (all objects)

// Arrays
const arr = [1, 2, 3];
console.log(arr instanceof Array);  // true
console.log(arr instanceof Object);  // true

// Primitives
console.log('hello' instanceof String);  // false (primitive, not object)
console.log(new String('hello') instanceof String);  // true (wrapper object)
console.log(42 instanceof Number);  // false

// instanceof can be fooled
function Fake() {}
Fake.prototype = dog;  // Set prototype to instance
const fake = new Fake();
console.log(fake instanceof Dog);  // true! (dog in prototype chain)

// Object.create
const parent = { type: 'parent' };
const child = Object.create(parent);
// No constructor function, instanceof doesn't work well
// console.log(child instanceof ???);  // What to check?

Example: isPrototypeOf() method

const animal = {
    eats: true
};

const rabbit = Object.create(animal);
rabbit.jumps = true;

const longEaredRabbit = Object.create(rabbit);
longEaredRabbit.earLength = 10;

// Check prototype relationships
console.log(animal.isPrototypeOf(rabbit));  // true
console.log(animal.isPrototypeOf(longEaredRabbit));  // true (anywhere in chain)
console.log(rabbit.isPrototypeOf(longEaredRabbit));  // true

console.log(rabbit.isPrototypeOf(animal));  // false (wrong direction)

// Works with any object, not just constructors
console.log(Object.prototype.isPrototypeOf(rabbit));  // true (all objects)
console.log(Object.prototype.isPrototypeOf({}));  // true

// Compare with instanceof (requires constructor)
function Animal() {}
const dog = new Animal();
console.log(dog instanceof Animal);  // true
console.log(Animal.prototype.isPrototypeOf(dog));  // true (equivalent)

// null prototype
const noProto = Object.create(null);
console.log(Object.prototype.isPrototypeOf(noProto));  // false

Example: Property ownership testing

const parent = {
    inherited: 'from parent',
    sharedMethod() {
        return 'shared';
    }
};

const child = Object.create(parent);
child.own = 'own property';

// 'in' operator - checks own and inherited
console.log('own' in child);  // true (own)
console.log('inherited' in child);  // true (inherited)
console.log('sharedMethod' in child);  // true (inherited)
console.log('nonexistent' in child);  // false

// hasOwnProperty - checks only own properties
console.log(child.hasOwnProperty('own'));  // true
console.log(child.hasOwnProperty('inherited'));  // false (inherited)
console.log(child.hasOwnProperty('sharedMethod'));  // false

// Object.hasOwn (ES2022) - safer alternative
console.log(Object.hasOwn(child, 'own'));  // true
console.log(Object.hasOwn(child, 'inherited'));  // false

// Why Object.hasOwn is safer
const obj = Object.create(null);  // No prototype
obj.prop = 'value';
// obj.hasOwnProperty('prop');  // Error: hasOwnProperty not available
console.log(Object.hasOwn(obj, 'prop'));  // true (works!)

// Distinguish own vs inherited in iteration
for (let key in child) {
    if (child.hasOwnProperty(key)) {
        console.log(`Own: ${key}`);
    } else {
        console.log(`Inherited: ${key}`);
    }
}
// Own: own
// Inherited: inherited
// Inherited: sharedMethod

// Object.keys() only returns own enumerable properties
console.log(Object.keys(child));  // ['own']

// Get all properties including inherited (manual)
function getAllProperties(obj) {
    const props = new Set();
    let current = obj;
    while (current) {
        Object.getOwnPropertyNames(current).forEach(p => props.add(p));
        current = Object.getPrototypeOf(current);
    }
    return Array.from(props);
}

console.log(getAllProperties(child));
// Includes: own, inherited, sharedMethod, toString, hasOwnProperty, etc.

Example: Type checking best practices

// Checking built-in types
function checkType(value) {
    // Arrays - use Array.isArray (most reliable)
    if (Array.isArray(value)) {
        return 'array';
    }
    
    // null (special case - typeof null === 'object')
    if (value === null) {
        return 'null';
    }
    
    // Primitives - use typeof
    if (typeof value !== 'object') {
        return typeof value;  // 'string', 'number', 'boolean', etc.
    }
    
    // Objects - check constructor
    if (value instanceof Date) return 'date';
    if (value instanceof RegExp) return 'regexp';
    if (value instanceof Map) return 'map';
    if (value instanceof Set) return 'set';
    
    return 'object';
}

console.log(checkType([1, 2, 3]));  // 'array'
console.log(checkType('hello'));  // 'string'
console.log(checkType(42));  // 'number'
console.log(checkType(null));  // 'null'
console.log(checkType(new Date()));  // 'date'
console.log(checkType({ x: 1 }));  // 'object'

// Duck typing - check for expected methods
function isIterable(obj) {
    return obj != null && typeof obj[Symbol.iterator] === 'function';
}

console.log(isIterable([1, 2, 3]));  // true
console.log(isIterable('hello'));  // true
console.log(isIterable({ x: 1 }));  // false
console.log(isIterable(new Map()));  // true

// Custom type guard (TypeScript-like pattern in JS)
function isDog(animal) {
    return animal 
        && typeof animal.bark === 'function'
        && typeof animal.breed === 'string';
}

const dog = { name: 'Rex', breed: 'Lab', bark() {} };
const cat = { name: 'Fluffy', meow() {} };

console.log(isDog(dog));  // true
console.log(isDog(cat));  // false

// Symbol.toStringTag for custom types
class CustomType {
    get [Symbol.toStringTag]() {
        return 'CustomType';
    }
}

const custom = new CustomType();
console.log(Object.prototype.toString.call(custom));  // '[object CustomType]'
console.log(Object.prototype.toString.call([]));  // '[object Array]'
console.log(Object.prototype.toString.call(new Date()));  // '[object Date]'
Common Pitfalls:
  • instanceof with primitives: Returns false for primitives like 'string' even though they can use String methods
  • instanceof across frames: Different window/frame has different built-in constructors; use Array.isArray() for arrays
  • hasOwnProperty safety: Objects with null prototype don't have hasOwnProperty; use Object.hasOwn() instead
  • typeof null: Returns 'object' (JavaScript quirk); check === null explicitly

6. Object Delegation Patterns

Pattern Description Key Concept
Behavior Delegation (OLOO) Objects delegate to other objects (not classes) Objects Linked to Other Objects - pure prototypal
Classical Inheritance Parent-child hierarchy with constructors Constructor functions + prototype chain
Concatenative Inheritance Mix properties from multiple sources Object.assign() / spread for composition
Functional Inheritance Factory functions with closures for privacy Functions return objects with private state
Aspect Classical (Constructor) Delegation (OLOO)
Syntax new Constructor() Object.create(proto)
Focus Parent-child classes Peer objects delegating to each other
Initialization Constructor function Init method on prototype
Complexity More verbose, mimics classes Simpler, embraces prototypes
Mental Model "This IS-A that" (inheritance) "This HAS-A reference to that" (delegation)

Example: OLOO pattern (Objects Linked to Other Objects)

// Behavior delegation style (OLOO)
const Vehicle = {
    init(make, model) {
        this.make = make;
        this.model = model;
        return this;
    },
    
    describe() {
        return `${this.make} ${this.model}`;
    }
};

const Car = Object.create(Vehicle);

Car.init = function(make, model, doors) {
    // Delegate to Vehicle.init
    Vehicle.init.call(this, make, model);
    this.doors = doors;
    return this;
};

Car.drive = function() {
    return `Driving ${this.describe()}`;
};

// Create instances
const myCar = Object.create(Car).init('Toyota', 'Camry', 4);
const yourCar = Object.create(Car).init('Honda', 'Civic', 4);

console.log(myCar.describe());  // "Toyota Camry"
console.log(myCar.drive());  // "Driving Toyota Camry"
console.log(yourCar.drive());  // "Driving Honda Civic"

// No constructors, no 'new', just objects delegating
console.log(Object.getPrototypeOf(myCar) === Car);  // true
console.log(Object.getPrototypeOf(Car) === Vehicle);  // true

// Compare with constructor pattern
function VehicleConstructor(make, model) {
    this.make = make;
    this.model = model;
}

VehicleConstructor.prototype.describe = function() {
    return `${this.make} ${this.model}`;
};

function CarConstructor(make, model, doors) {
    VehicleConstructor.call(this, make, model);
    this.doors = doors;
}

CarConstructor.prototype = Object.create(VehicleConstructor.prototype);
CarConstructor.prototype.constructor = CarConstructor;

CarConstructor.prototype.drive = function() {
    return `Driving ${this.describe()}`;
};

const constructorCar = new CarConstructor('Ford', 'Focus', 4);
console.log(constructorCar.drive());  // Same result, more complex setup

Example: Concatenative inheritance (composition)

// Reusable behavior objects
const canEat = {
    eat(food) {
        return `${this.name} eats ${food}`;
    }
};

const canWalk = {
    walk() {
        return `${this.name} walks`;
    }
};

const canSwim = {
    swim() {
        return `${this.name} swims`;
    }
};

// Compose objects by combining behaviors
function createDog(name) {
    return {
        name,
        ...canEat,
        ...canWalk,
        bark() {
            return `${this.name} barks`;
        }
    };
}

function createFish(name) {
    return {
        name,
        ...canEat,
        ...canSwim
    };
}

function createDuck(name) {
    return {
        name,
        ...canEat,
        ...canWalk,
        ...canSwim,
        quack() {
            return `${this.name} quacks`;
        }
    };
}

const dog = createDog('Rex');
console.log(dog.eat('bone'));  // "Rex eats bone"
console.log(dog.walk());  // "Rex walks"
console.log(dog.bark());  // "Rex barks"
// console.log(dog.swim());  // undefined (no swim capability)

const duck = createDuck('Donald');
console.log(duck.eat('bread'));  // "Donald eats bread"
console.log(duck.walk());  // "Donald walks"
console.log(duck.swim());  // "Donald swims"
console.log(duck.quack());  // "Donald quacks"

// Advanced: functional mixin
const withLogging = (obj) => {
    const originalMethods = {};
    
    Object.keys(obj).forEach(key => {
        if (typeof obj[key] === 'function') {
            originalMethods[key] = obj[key];
            obj[key] = function(...args) {
                console.log(`Calling ${key} with`, args);
                return originalMethods[key].apply(this, args);
            };
        }
    });
    
    return obj;
};

const loggedDog = withLogging(createDog('Max'));
loggedDog.eat('treat');  // Logs: "Calling eat with ['treat']"
                         // Returns: "Max eats treat"

Example: Functional inheritance with closures

// Factory function with private state
function createBankAccount(initialBalance) {
    // Private variables (closure)
    let balance = initialBalance;
    let transactionHistory = [];
    
    // Private function
    function recordTransaction(type, amount) {
        transactionHistory.push({
            type,
            amount,
            timestamp: Date.now(),
            balance
        });
    }
    
    // Public interface
    return {
        deposit(amount) {
            if (amount > 0) {
                balance += amount;
                recordTransaction('deposit', amount);
            }
            return balance;
        },
        
        withdraw(amount) {
            if (amount > 0 && amount <= balance) {
                balance -= amount;
                recordTransaction('withdraw', amount);
            }
            return balance;
        },
        
        getBalance() {
            return balance;
        },
        
        getHistory() {
            // Return copy to prevent external modification
            return [...transactionHistory];
        }
    };
}

const account = createBankAccount(1000);
account.deposit(500);   // 1500
account.withdraw(200);  // 1300

console.log(account.getBalance());  // 1300
console.log(account.balance);  // undefined (private!)
console.log(account.getHistory().length);  // 2 transactions

// Inheritance with functional pattern
function createSavingsAccount(initialBalance, interestRate) {
    // Get base account
    const account = createBankAccount(initialBalance);
    
    // Add new functionality
    account.applyInterest = function() {
        const interest = account.getBalance() * interestRate;
        account.deposit(interest);
        return account.getBalance();
    };
    
    return account;
}

const savings = createSavingsAccount(1000, 0.05);
savings.deposit(500);  // 1500
savings.applyInterest();  // 1575 (5% interest)
console.log(savings.getBalance());  // 1575

Example: Practical delegation patterns

// Plugin system using delegation
const BaseApp = {
    init(name) {
        this.name = name;
        this.plugins = [];
        return this;
    },
    
    use(plugin) {
        this.plugins.push(plugin);
        if (plugin.install) {
            plugin.install(this);
        }
        return this;
    },
    
    run() {
        console.log(`${this.name} is running`);
        this.plugins.forEach(p => p.onRun && p.onRun(this));
    }
};

// Specific app type
const WebApp = Object.create(BaseApp);
WebApp.init = function(name, port) {
    BaseApp.init.call(this, name);
    this.port = port;
    return this;
};

WebApp.start = function() {
    console.log(`Server started on port ${this.port}`);
    this.run();
};

// Plugins
const LoggerPlugin = {
    install(app) {
        console.log(`Logger installed in ${app.name}`);
    },
    onRun(app) {
        console.log(`[LOG] ${app.name} executed`);
    }
};

const CachePlugin = {
    install(app) {
        app.cache = new Map();
        console.log(`Cache installed in ${app.name}`);
    },
    onRun(app) {
        console.log(`[CACHE] Size: ${app.cache.size}`);
    }
};

// Create app with plugins
const app = Object.create(WebApp)
    .init('MyApp', 3000)
    .use(LoggerPlugin)
    .use(CachePlugin);

app.start();
// Logger installed in MyApp
// Cache installed in MyApp
// Server started on port 3000
// MyApp is running
// [LOG] MyApp executed
// [CACHE] Size: 0

Section 8 Summary

  • Prototype chain: Property lookup traverses [[Prototype]] links from object to Object.prototype to null; use getPrototypeOf(), not __proto__
  • Constructor pattern: Function with new creates instances; 4 steps: create, link prototype, execute, return; methods on prototype for efficiency
  • Object.create: Direct prototypal inheritance without constructors; clean delegation; use for pure prototypal patterns
  • Method override: Child methods shadow parent; call parent with super or Parent.prototype.method.call(this); enables polymorphism
  • Type testing: instanceof checks constructor chain; isPrototypeOf() checks object chain; Object.hasOwn() for property ownership (safer than hasOwnProperty)
  • Delegation patterns: OLOO for pure delegation; concatenative for composition; functional for privacy; choose based on needs