JavaScript

Prototypes & Inheritance

20 Questions

Every JavaScript object has an internal [[Prototype]] link to another object. When accessing a property, the engine searches the object first, then its prototype, then the prototype's prototype, until it reaches null. This linked series of objects is called the prototype chain. It is the fundamental mechanism behind inheritance in JavaScript — objects can share behavior through their prototype chain. All objects ultimately inherit from Object.prototype, which has null as its prototype — this is the end of every prototype chain. Here is how the prototype chain works:
const animal = { eats: true };
const dog = Object.create(animal);
dog.barks = true;

console.log(dog.barks); // true (own property)
console.log(dog.eats);  // true (found on prototype)
console.log(dog.flies); // undefined (not in chain)

// Chain: dog -> animal -> Object.prototype -> null
console.log(Object.getPrototypeOf(dog) === animal); // true
console.log(Object.getPrototypeOf(animal) === Object.prototype); // true
console.log(Object.getPrototypeOf(Object.prototype)); // null
Property lookup traverses the chain from the object upward — the first match is returned. If no match is found in the entire chain, undefined is returned. Setting a property always creates it on the object itself (not on the prototype), even if a property with the same name exists higher in the chain.

Why it matters: The prototype chain is the fundamental mechanism of inheritance in JavaScript. Every object, function, and array uses the prototype chain. Understanding it explains how methods like .map(), .toString(), and .hasOwnProperty() are accessible on all values.

Real applications: Debugging why a method is available on all instances (it's on the prototype), understanding why overwriting a prototype method affects all instances, implementing prototype-based mixins, and understanding framework behavior like Vue's reactive getters or Backbone's model methods.

Common mistakes: Thinking setting a property on an object changes the prototype (it creates an own property and shadows the prototype), not knowing that null is the end of the chain (Object.prototype's prototype is null), and confusing the prototype chain lookup (reading) with property assignment (always own).

__proto__ is the accessor property on every object that points to its prototype. prototype is a property on constructor functions that is used to set the __proto__ of instances created with new. These two are often confused. Every function has a prototype property (used when called with new), and every object has a __proto__ property (pointing to its prototype). In modern code, use Object.getPrototypeOf() instead of __proto__ because __proto__ is a legacy feature that may not be supported in all environments. Here is how they relate:
function Person(name) { this.name = name; }
Person.prototype.greet = function() { return "Hi, " + this.name; };

const p = new Person("Alice");
console.log(p.__proto__ === Person.prototype);        // true
console.log(Object.getPrototypeOf(p) === Person.prototype); // true
console.log(p.greet()); // "Hi, Alice"

// prototype is only on functions
console.log(typeof Person.prototype); // "object"
console.log(typeof p.prototype);      // "undefined" — p is not a function
When you call new Person(), JavaScript creates a new object and sets its __proto__ to Person.prototype, then executes the constructor with this bound to the new object. Use Object.setPrototypeOf() to change an object's prototype, but be aware this is a slow operation that should be avoided in performance-critical code.

Why it matters: Understanding the difference between __proto__ (instance link to prototype) and .prototype (constructor's prototype object) is a common interview question. The confusion between the two is one of the most frequent JavaScript misunderstandings.

Real applications: Inspecting prototype chains in the browser DevTools, using Object.getPrototypeOf() to traverse the chain programmatically, understanding why Array.prototype.slice can be borrowed by other array-like objects, and diagnosing instanceof failures across iframes (different prototype chain).

Common mistakes: Using __proto__ directly in production code (it's deprecated — use Object.getPrototypeOf/setPrototypeOf), confusing Foo.prototype (the object instances inherit from) with foo.__proto__ (the actual prototype link on the instance), and changing prototype mid-lifecycle which defeats V8 shape optimizations.

Object.create(proto) creates a new object with its [[Prototype]] set to the specified object. It enables prototypal inheritance without using constructors or the class keyword. The second optional argument accepts property descriptors that define properties on the new object with fine-grained control over writability, enumerability, and configurability. Object.create(null) creates an object with no prototype at all — useful for creating clean dictionaries without inherited methods like toString or hasOwnProperty. Here is how Object.create() works:
const vehicle = {
  start() { return this.type + " started"; }
};

const car = Object.create(vehicle);
car.type = "Car";
console.log(car.start()); // "Car started"

// Create with property descriptors
const bike = Object.create(vehicle, {
  type: { value: "Bike", writable: true, enumerable: true }
});
console.log(bike.start()); // "Bike started"

// Null prototype — clean dictionary
const dict = Object.create(null);
dict.key = "value";
console.log(dict.toString); // undefined — no inherited methods
Object.create() is the purest form of prototypal inheritance — it directly links objects without constructor functions or class syntax. Use Object.create(null) when you need a plain key-value store without risk of prototype pollution attacks.

Why it matters: Object.create() is the explicit prototype-control primitive. It reveals how ES6 class inheritance actually works under the hood — extends sets up exactly the prototype chain that Object.create() can build manually. It's also a common interview question for demonstrating deep prototype knowledge.

Real applications: Implementing classical inheritance in ES5 (the pre-class pattern), creating prototype-safe dictionaries with Object.create(null), building mixin libraries, subclassing built-ins, and manually setting up the correct inheritance chain after separating constructor logic.

Common mistakes: Not calling the parent constructor (Object.create sets up prototype but doesn't run constructor), thinking Object.create(Foo.prototype) is equivalent to new Foo() (constructor is not called), and using Object.create(null) objects with methods like toString() or hasOwnProperty() (they don't inherit those).

ES6 class is syntactic sugar over prototype-based inheritance. Under the hood, classes are still constructor functions with prototypes — but they provide a much cleaner syntax for defining constructors, methods, static members, and getters/setters. Classes support constructor methods, instance methods, static methods, getters/setters, and private fields (with # prefix in ES2022). Unlike function declarations, classes are not hoisted. Classes must be called with new — calling them without new throws a TypeError, unlike constructor functions which silently run in the global scope. Here is how classes work:
class Animal {
  constructor(name) {
    this.name = name;
  }
  speak() {
    return this.name + " makes a sound";
  }
  static create(name) {
    return new Animal(name);
  }
  get info() {
    return "Animal: " + this.name;
  }
}

const a = new Animal("Dog");
console.log(a.speak());        // "Dog makes a sound"
console.log(a.info);           // "Animal: Dog"
console.log(typeof Animal);    // "function" (still a function!)
console.log(Animal.create("Cat").name); // "Cat"
Class methods are defined on Class.prototype, so all instances share the same method references — just like adding methods to a constructor function's prototype. Static methods belong to the class itself, not to instances — they are called on the class directly (like Array.isArray() or Object.keys()).

Why it matters: ES6 classes provide a clean syntax for constructor functions and prototype-based inheritance that eliminates the verbose pre-ES6 setup. Under the hood, classes are syntactic sugar over the same prototype mechanisms, but they enforce correct usage (must use new, super() before this in subclasses).

Real applications: Modeling domain entities (User, Product, Order), building React class components (legacy), implementing the Repository pattern for data access, and creating service objects with lifecycle hooks in Angular (which uses class-based DI).

Common mistakes: Calling a class without new (TypeError in strict class mode, unlike function constructors), forgetting that class methods are non-enumerable (unlike manually assigned prototype methods), and thinking class declarations are hoisted like function declarations (they're in the TDZ until evaluated).

The extends keyword creates a subclass that inherits from a parent class. The child class inherits all methods and can override them or add new ones. The prototype chain is set up automatically. When a child class defines a method with the same name as a parent method, the child's method overrides the parent's. You can still access the parent method using super.methodName(). JavaScript only supports single inheritance — a class can only extend one parent class. For multiple inheritance, use mixins instead. Here is how class inheritance works:
class Animal {
  constructor(name) { this.name = name; }
  speak() { return this.name + " makes a sound"; }
}

class Dog extends Animal {
  speak() { return this.name + " barks"; }
  fetch(item) { return this.name + " fetches " + item; }
}

const d = new Dog("Rex");
console.log(d.speak());       // "Rex barks" (overridden)
console.log(d.fetch("ball")); // "Rex fetches ball"
console.log(d instanceof Animal); // true
console.log(d instanceof Dog);    // true

// Prototype chain: d -> Dog.prototype -> Animal.prototype -> Object.prototype
The instanceof operator checks the entire prototype chain — a Dog instance is also an instance of Animal and Object. You can extend built-in classes too: class MyArray extends Array { } creates a custom array class with additional methods.

Why it matters: extends sets up both the instance prototype chain AND the static inheritance (so Child.staticMethod also inherits from Parent.staticMethod). This is the correct way to achieve inheritance in modern JavaScript and is used extensively in TypeScript and framework subclassing.

Real applications: Extending Error for custom error classes, extending React.Component, extending EventEmitter in Node.js, building a hierarchy of domain models (Vehicle → Car → ElectricCar), and implementing Abstract Base Class patterns with protected interface contracts.

Common mistakes: Forgetting to call super() before accessing this in a subclass constructor (ReferenceError), thinking you can override a method and then call this.method() to get the parent version (need super.method()), and extending built-ins like Array in ES5 environments (doesn't work correctly without transpiler fixes).

super() calls the parent class constructor — required in a child constructor before using this. super.method() calls a parent method by name, allowing you to extend rather than fully override behavior.
class Shape {
  constructor(color) { this.color = color; }
  describe() { return `A ${this.color} shape`; }
}

class Circle extends Shape {
  constructor(color, radius) {
    super(color); // must call before using this
    this.radius = radius;
  }
  describe() {
    return `${super.describe()} with radius ${this.radius}`;
  }
}

const c = new Circle("red", 5);
console.log(c.describe()); // "A red shape with radius 5"

Why it matters: super is essential for subclass constructors and method overriding. Without super() in a subclass constructor, JavaScript throws a ReferenceError before this is accessible. Understanding super unlocks proper multilevel inheritance patterns.

Real applications: Extending React.Component and calling super(props), adding validation in a subclass constructor before calling parent constructor, overriding toString() or render() while calling the parent version for base behavior, and building layered error classes with context-specific messages.

Common mistakes: Accessing this before super() in a subclass constructor (ReferenceError), forgetting super() entirely in a constructor (also a ReferenceError), and using super.method() in an arrow function (won't work — super is lexically unavailable in arrow functions inside static methods).

instanceof checks whether an object's prototype chain includes the prototype property of a constructor. It walks up the chain and returns true if found.
class Animal {}
class Dog extends Animal {}

const d = new Dog();
console.log(d instanceof Dog);    // true
console.log(d instanceof Animal); // true
console.log(d instanceof Object); // true

// Works with constructor functions too
function Cat() {}
const c = new Cat();
console.log(c instanceof Cat); // true

Why it matters: instanceof is the standard way to perform type-based branching on objects. It's used in error handling, polymorphic dispatch, and library APIs that need to differentiate between object types. Understanding its prototype-chain mechanism explains edge cases like cross-frame failures.

Real applications: Checking error instanceof TypeError in catch blocks, dispatching based on object type in visitor patterns, checking value instanceof Promise before awaiting, validating constructor arguments at runtime, and implementing type narrowing in TypeScript-style guards.

Common mistakes: Using instanceof across iframes or web workers (different prototype chains, always returns false), not knowing Symbol.hasInstance can override instanceof behavior, thinking typeof works like instanceof for objects (typeof returns 'object' for all non-primitives), and relying on instanceof instead of duck typing when dealing with objects from different realms.

hasOwnProperty() returns true only if the property exists directly on the object, not inherited through the prototype chain. It is essential for distinguishing own vs inherited properties, especially in for...in loops.
const parent = { inherited: true };
const child = Object.create(parent);
child.own = true;

console.log(child.hasOwnProperty("own"));       // true
console.log(child.hasOwnProperty("inherited")); // false
console.log("inherited" in child);              // true

for (const key in child) {
  if (child.hasOwnProperty(key)) {
    console.log(key); // Only "own"
  }
}

Why it matters: In for...in loops, JavaScript walks the entire prototype chain including inherited properties. Without a hasOwnProperty guard, you risk processing inherited methods or properties that were accidentally added to Object.prototype. Modern code prefers Object.hasOwn() for null-prototype object safety.

Real applications: Safely iterating object properties in utility functions, checking if a default config was explicitly overridden vs inherited, validating that form data keys exist as own properties on a schema, and auditing which properties were added at runtime vs inherited from a prototype.

Common mistakes: Calling obj.hasOwnProperty() on objects without the method (null-prototype objects — use Object.hasOwn() instead), using key in obj when you mean own properties only (in checks inherited too), and not realizing Object.keys() already filters to own enumerable properties (no guard needed for Object.keys loops).

Mixins allow you to add functionality from multiple sources to a class since JavaScript only supports single inheritance. Use Object.assign() to copy methods onto a class prototype.
const Serializable = {
  serialize() { return JSON.stringify(this); }
};

const Loggable = {
  log() { console.log(`[${this.constructor.name}]`, this); }
};

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

Object.assign(User.prototype, Serializable, Loggable);

const user = new User("Alice");
console.log(user.serialize()); // '{"name":"Alice"}'
user.log(); // [User] User { name: "Alice" }

Why it matters: JavaScript's single-inheritance-only class model (one parent class) would be limiting without mixins. Mixins are the standard workaround for reusing behavior across unrelated class hierarchies, similar to traits in PHP or interfaces with default methods in Java.

Real applications: Adding serialization behavior to multiple model classes, implementing logging or event-emitting capabilities without inheritance, building plugin systems where capabilities can be composed, and the common React pattern of higher-order components as functional mixins.

Common mistakes: Method name collisions when multiple mixins define the same method (last one wins silently), forgetting that Object.assign copies methods by reference (shared state), using mixins when composition would be cleaner (a service class that delegates), and not knowing that class-based mixins using higher-order functions avoid the prototype mutation concern.

Classical inheritance (Java, C++) uses classes as blueprints to create instances — classes define the structure, and instances are copies of that structure. Prototypal inheritance (JavaScript) has objects that directly inherit from other objects through prototype links. In JavaScript, there are no true classes — ES6 class syntax is syntactic sugar over prototype-based inheritance. Objects are linked to other objects, not instantiated from class blueprints. Prototypal inheritance is more flexible — you can add or modify prototype methods at runtime, and objects can delegate behavior to any other object. Here is the comparison:
// Prototypal — objects inherit from objects
const proto = {
  greet() { return "Hello from " + this.name; }
};
const obj = Object.create(proto);
obj.name = "Alice";
obj.greet(); // "Hello from Alice"

// Class syntax (prototypal under the hood)
class Person {
  constructor(name) { this.name = name; }
  greet() { return "Hello from " + this.name; }
}
// Person.prototype.greet exists on the prototype

// Dynamic modification (prototypal advantage)
proto.farewell = function() { return "Bye from " + this.name; };
obj.farewell(); // "Bye from Alice" — works immediately!
Prototypal inheritance uses a delegation model — objects don't copy methods from their prototypes, they delegate method calls up the chain. Most modern JavaScript code uses class syntax for familiarity, but understanding prototypes is essential for debugging and advanced patterns.

Why it matters: Knowing that JavaScript's classes are prototype-based explains behaviors that seem strange otherwise — like why you can add a method to Array.prototype and all arrays get it, or why changing a method on the prototype of an already-created instance works immediately.

Real applications: Library design that extends prototypes (utility libraries), monkey-patching for polyfills, understanding why React hooks replaced class components (closures vs prototype-bound methods), and mixing patterns like Object.create for delegation vs class for structure.

Common mistakes: Treating class methods as copied to each instance (they're shared on the prototype — arrow function class fields ARE per-instance), assuming class inheritance is deep copying (it's dynamic prototype linking), and not understanding that the prototype chain is live (adding to a prototype after instances are created still works).

ES2022 introduced private class fields using the # prefix. Private fields and methods are only accessible within the class body — any access from outside throws a SyntaxError. Before private fields, developers used closures, WeakMaps, or naming conventions (underscore prefix) to simulate privacy. The # syntax provides true language-level privacy. Private fields are not inherited by subclasses and cannot be accessed even through the prototype chain or reflection APIs. Here is how private class fields work:
class BankAccount {
  #balance = 0;  // private field
  #pin;          // private field

  constructor(pin, initialBalance = 0) {
    this.#pin = pin;
    this.#balance = initialBalance;
  }

  #validatePin(pin) {  // private method
    return pin === this.#pin;
  }

  deposit(amount) {
    if (amount > 0) this.#balance += amount;
  }

  withdraw(amount, pin) {
    if (this.#validatePin(pin) && amount <= this.#balance) {
      this.#balance -= amount;
      return amount;
    }
    return 0;
  }

  get balance() { return this.#balance; }
}

const account = new BankAccount("1234", 100);
account.deposit(50);
console.log(account.balance); // 150
// account.#balance; // SyntaxError: Private field
Private fields use a hard privacy model — even subclasses, debuggers, and Object.keys()/Reflect cannot access them. Use private fields for data that should never be exposed, and use closures or WeakMaps for compatibility with older environments.

Why it matters: Private class fields (#field) enforce true encapsulation at the language level — unlike the convention of prefixing with underscore (_field), which is just a naming hint. This prevents accidental or malicious access and is part of the modern JavaScript class features specification.

Real applications: Storing bank account balances, authentication tokens, internal state in UI components, memoization caches, and any sensitive data that should never be exposed via serialization or external access.

Common mistakes: Accessing private fields in subclasses (they cannot — private fields are NOT inherited), trying to access private fields from outside the class in tests (use getters or extract to separate method instead), and using WeakMap-based privacy patterns when # fields are already supported and much cleaner.

Static methods and properties belong to the class itself, not to instances. They are called on the class directly and are commonly used for utility functions, factory methods, and constants. Static methods cannot access this referring to an instance — inside a static method, this refers to the class constructor itself. Static members are inherited by subclasses — a child class can call static methods defined on its parent class. Here is how static members work:
class MathHelper {
  static PI = 3.14159;  // static property

  static square(x) { return x * x; }  // static method
  static cube(x) { return x * x * x; }

  static isEven(n) { return n % 2 === 0; }
}

console.log(MathHelper.PI);          // 3.14159
console.log(MathHelper.square(5));   // 25
console.log(MathHelper.isEven(4));   // true

// Cannot use on instances
const helper = new MathHelper();
// helper.square(5);  // TypeError: helper.square is not a function

// Static factory method
class User {
  constructor(name, role) {
    this.name = name;
    this.role = role;
  }
  static createAdmin(name) {
    return new User(name, "admin");
  }
  static createGuest() {
    return new User("Guest", "guest");
  }
}
Common examples of static methods in built-in classes include Array.isArray(), Object.keys(), Number.parseInt(), and JSON.parse(). The factory pattern with static methods is a clean way to create instances with different configurations without exposing complex constructor logic.

Why it matters: Static methods are fundamental to JavaScript's API design. Every utility on Array, Object, Number, and JSON is static. Understanding when to use static vs instance methods is key to clean class design and is a common interview question.

Real applications: Factory methods like User.createAdmin(), singleton accessors like Database.getInstance(), utility classes grouping related functions (MathHelper), framework-level static constructors like Observable.from(), and class-level caching with a static Map or WeakMap.

Common mistakes: Calling static methods on instances (TypeError), not knowing static methods are inherited by subclasses (Child.inherited StaticMethod() works), using this in a static method expecting it to be an instance (it's the class/constructor itself), and adding too much logic to static factory methods making them hard to test.

Getters and setters are special methods that allow you to define computed properties — they look like regular property access but execute custom logic behind the scenes. get defines a method called when the property is read. set defines a method called when the property is assigned. Together, they enable validation, computed values, and lazy initialization. Getters and setters work in both object literals and class definitions. They appear as regular properties to external code. Here is how getters and setters work:
class Temperature {
  #celsius;

  constructor(celsius) {
    this.#celsius = celsius;
  }

  get fahrenheit() {
    return this.#celsius * 9/5 + 32;  // computed property
  }

  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);
console.log(temp.fahrenheit); // 212 (getter)
temp.fahrenheit = 32;         // setter
console.log(temp.celsius);    // 0

// In object literals
const user = {
  firstName: "John",
  lastName: "Doe",
  get fullName() { return this.firstName + " " + this.lastName; },
  set fullName(name) {
    [this.firstName, this.lastName] = name.split(" ");
  }
};
Getters are useful for computed properties that derive from other data — like fullName from firstName and lastName. Setters provide a natural place for validation logic — you can reject invalid values before they are stored.

Why it matters: Getters and setters allow objects to present a clean external API while hiding internal implementation details. They're how Vue 2's reactivity system worked — wrapping property access to trigger dependency tracking and re-rendering.

Real applications: Temperature/currency unit conversion (read in one unit, store in another), form model validation (setter rejects invalid email format), computed properties like fullName from firstName+lastName, lazy evaluation (compute on first access, then cache), and Observable property patterns in reactive frameworks.

Common mistakes: Creating infinite recursion by reading this.celsius inside a getter named celsius (use a private backing field like #celsius), not knowing that getters/setters defined with object literal syntax are non-enumerable, and forgetting a getter without a setter is read-only (attempting to set it silently fails in non-strict mode).

Every property in JavaScript has a property descriptor that defines its behavior. Descriptors control whether a property is writable, enumerable, and configurable. Data descriptors have value and writable. Accessor descriptors have get and set. Both types share enumerable and configurable. Object.defineProperty() lets you create properties with custom descriptors, giving fine-grained control over property behavior. Here is how property descriptors work:
const obj = {};

// Define a non-writable, non-enumerable property
Object.defineProperty(obj, "id", {
  value: 42,
  writable: false,      // cannot be changed
  enumerable: false,    // hidden from for...in and Object.keys
  configurable: false   // cannot be deleted or reconfigured
});

console.log(obj.id);     // 42
obj.id = 100;            // silently fails (strict mode: TypeError)
console.log(obj.id);     // 42

// Get descriptor
const desc = Object.getOwnPropertyDescriptor(obj, "id");
console.log(desc);
// { value: 42, writable: false, enumerable: false, configurable: false }

// Define multiple properties
Object.defineProperties(obj, {
  name: { value: "Alice", writable: true, enumerable: true },
  age:  { value: 30, writable: true, enumerable: true }
});
Properties created with regular assignment have all descriptor flags set to true. Properties created with Object.defineProperty() default all flags to false. Understanding descriptors is key to understanding Object.freeze(), Object.seal(), and how libraries create non-enumerable utility methods.

Why it matters: Property descriptors are the low-level metadata layer under every property in JavaScript. They explain why some properties don't appear in loops, why some can't be deleted, and how frameworks create "invisible" helper properties on objects. Without understanding descriptors, many framework behaviors seem magical.

Real applications: Creating read-only versioning constants (writable: false), adding non-enumerable internal metadata to objects, implementing Vue 2-style reactive getters/setters via defineProperty, building sealed API objects where keys can't be added or removed, and auditing third-party code that patches objects.

Common mistakes: Not knowing defineProperty defaults all flags to false (regular assignment defaults all to true), trying to redefine a non-configurable property (TypeError), and not realizing that enumerable: false hides from Object.keys() but not from Object.getOwnPropertyNames().

The new keyword creates a new instance of a constructor function or class. It performs four steps internally that set up the object and its prototype chain. Step 1: Create a new empty object. Step 2: Set the object's [[Prototype]] to the constructor's prototype. Step 3: Execute the constructor with this bound to the new object. Step 4: Return the object (unless the constructor returns a different object). If the constructor explicitly returns an object, that object is used instead. If it returns a primitive, the primitive is ignored and the new object is returned. Here is what new does step by step:
function Person(name) {
  // Step 3: this = new object
  this.name = name;
  // Step 4: return this (implicit)
}
Person.prototype.greet = function() {
  return "Hi, " + this.name;
};

const p = new Person("Alice");
// Step 1: {} created
// Step 2: {}.__proto__ = Person.prototype
// Step 3: this.name = "Alice"
// Step 4: return { name: "Alice" }

console.log(p.greet()); // "Hi, Alice"

// What new does (manual implementation)
function myNew(Constructor, ...args) {
  const obj = Object.create(Constructor.prototype); // Steps 1-2
  const result = Constructor.apply(obj, args);      // Step 3
  return result instanceof Object ? result : obj;   // Step 4
}

const p2 = myNew(Person, "Bob");
console.log(p2.greet()); // "Hi, Bob"
Calling a constructor without new executes it as a regular function — this points to the global object (or undefined in strict mode), leading to bugs. ES6 classes enforce the use of new — calling a class without new throws a TypeError, preventing this common mistake.

Why it matters: The new keyword's four-step behavior is a core JavaScript mechanism. Implementing myNew() manually is a classic interview question that verifies deep understanding of prototype linking, constructor binding, and the return value rules.

Real applications: Understanding why React hooks enforce "call at top level" (not inside conditionals) by modeling component state similarly, debugging why this is undefined in constructor functions called without new, understanding what frameworks do when they call your class constructor internally, and implementing dependency injection containers that use new dynamically.

Common mistakes: Calling a constructor function without new in non-strict mode (accidentally sets properties on global), forgetting that if a constructor returns an object, you get that object back (not the created instance), and not knowing that new.target lets constructors detect whether they were called with new.

Prototype pollution is a security vulnerability where an attacker modifies Object.prototype (or another prototype), affecting all objects that inherit from it. This can lead to property injection, denial of service, or remote code execution. It typically happens when user input is used to set properties on objects without validation — especially through functions that recursively merge objects or set nested properties using string paths. This is a serious security concern listed in the OWASP Top 10 and has affected major libraries like Lodash and jQuery. Here is how prototype pollution works and how to prevent it:
// Vulnerable: recursive merge without checks
function merge(target, source) {
  for (const key in source) {
    if (typeof source[key] === "object") {
      target[key] = target[key] || {};
      merge(target[key], source[key]);
    } else {
      target[key] = source[key];
    }
  }
}

// Attack
const malicious = JSON.parse('{"__proto__":{"isAdmin":true}}');
merge({}, malicious);
console.log({}.isAdmin); // true — ALL objects affected!

// Prevention 1: Check for dangerous keys
function safeMerge(target, source) {
  for (const key of Object.keys(source)) {
    if (key === "__proto__" || key === "constructor" || key === "prototype") {
      continue; // skip dangerous keys
    }
    target[key] = source[key];
  }
}

// Prevention 2: Use Object.create(null) for dictionaries
const safe = Object.create(null);
// No prototype chain to pollute
Always validate property names from user input — reject __proto__, constructor, and prototype keys. Use Object.create(null) for data dictionaries, Map for key-value stores, and Object.freeze(Object.prototype) in sensitive environments.

Why it matters: Prototype pollution is a serious real-world vulnerability (CVE-level issues in lodash, jQuery, etc.). It's also an OWASP-listed concern. Understanding how to prevent it demonstrates security awareness that is valued in senior engineering interviews.

Real applications: Validating and sanitizing user-supplied keys before object assignment, using Map instead of plain objects for user-defined key-value data, freezing Object.prototype in security-sensitive Node.js services, and patching libraries with prototype pollution vulnerabilities using Content Security Policy or runtime guards.

Common mistakes: Using user-controlled input as object keys without validation, not knowing that __proto__, constructor, and prototype are the most common attack vectors, thinking JSON.parse() is safe from prototype pollution (it can be if using a suitable schema validator), and using lodash merge/cloneDeep from old versions that were vulnerable.

JavaScript provides several ways to check an object's type — each with different strengths. instanceof checks the prototype chain, constructor checks the constructor function, and Symbol.toStringTag allows custom type strings. instanceof is the most common approach but fails across different execution contexts (iframes, workers). constructor can be overwritten. Custom type checking methods are most reliable. For built-in types, use dedicated methods like Array.isArray(), Number.isFinite(), and typeof for primitives. Here are the different type-checking approaches:
class Animal {}
class Dog extends Animal {}
const d = new Dog();

// instanceof — checks prototype chain
console.log(d instanceof Dog);    // true
console.log(d instanceof Animal); // true

// constructor property
console.log(d.constructor === Dog);    // true
console.log(d.constructor === Animal); // false

// Object.prototype.toString
console.log(Object.prototype.toString.call(d)); // "[object Object]"

// Custom toString tag
class Cat {
  get [Symbol.toStringTag]() { return "Cat"; }
}
const c = new Cat();
console.log(Object.prototype.toString.call(c)); // "[object Cat]"

// Type checking utilities
const isType = (obj, type) => obj?.constructor === type;
console.log(isType(d, Dog));    // true
console.log(isType([], Array)); // true
For cross-frame type checking, use duck typing — check for the presence of specific methods or properties rather than relying on instanceof. The Symbol.toStringTag property customizes the string returned by Object.prototype.toString.call() — useful for creating identifiable custom types.

Why it matters: Reliable type checking is a fundamental operation in JavaScript. Each mechanism has tradeoffs — instanceof fails across frames, constructor can be overwritten, typeof returns 'object' for all non-primitives. Knowing when to use which method is key to writing robust type-safe code.

Real applications: Array.isArray() as the standard cross-frame array check, Symbol.toStringTag for custom type-aware logging utilities, instanceof in catch blocks to filter specific error types, and duck typing in utility functions to avoid coupling to specific classes.

Common mistakes: Using instanceof across different browser frames/contexts (always fails — use Array.isArray() instead), relying on the constructor property when it could have been overwritten, and using typeof for object type checks (returns 'object' for arrays, null, and all non-primitives).

Method overriding occurs when a child class defines a method with the same name as a method in the parent class. The child's version takes precedence when called on child instances. You can call the parent's overridden method using super.methodName() — this allows you to extend the parent's behavior rather than completely replacing it. Method overriding works because JavaScript looks up methods starting from the object itself, then moves up the prototype chain. The first match is used. Here is how method overriding works:
class Shape {
  area() { return 0; }
  describe() { return "I am a shape"; }
}

class Rectangle extends Shape {
  constructor(width, height) {
    super();
    this.width = width;
    this.height = height;
  }
  area() { return this.width * this.height; } // override
  describe() {
    return super.describe() + " (rectangle " + this.width + "x" + this.height + ")";
  }
}

class Square extends Rectangle {
  constructor(side) {
    super(side, side);
  }
  describe() {
    return super.describe() + " — actually a square!";
  }
}

const sq = new Square(5);
console.log(sq.area());     // 25
console.log(sq.describe()); // "I am a shape (rectangle 5x5) — actually a square!"
The super keyword creates a chain of method calls up the prototype hierarchy — this is how you can build behavior incrementally. Method overriding is the basis for polymorphism — different classes can share the same method name but implement different behavior.

Why it matters: Method overriding is a core OOP concept that enables polymorphism. Without it, every "type" of object would need unique method names to handle different behavior, making code inflexible and verbose.

Real applications: Overriding toString() in domain models for debugging, overriding render() or componentDidMount() in React class components, implementing template method pattern (base class defines algorithm, subclasses override specific steps), and overriding equals() or validate() in form models.

Common mistakes: Not calling super.method() when you want to extend rather than replace parent behavior, overriding a method that returns a different type unexpectedly (Liskov substitution violation), and accidentally overriding a method by naming a property the same as an inherited method.

Object.assign() copies only own enumerable properties from source objects to a target object. It does NOT copy properties from the prototype chain, and it does NOT set up prototype links. This is important to understand — Object.assign() performs a shallow copy of property values, including methods. The copied methods become own properties of the target, not linked through the prototype. Object.assign() is commonly used for mixing in behavior, cloning objects, and merging configuration. Here is how Object.assign() interacts with prototypes:
const proto = { inherited: true };
const source = Object.create(proto);
source.own = "mine";

const target = {};
Object.assign(target, source);
console.log(target.own);       // "mine" — copied
console.log(target.inherited); // undefined — NOT copied!

// Copying methods
const mixin = {
  serialize() { return JSON.stringify(this); },
  clone() { return Object.assign({}, this); }
};

class User {
  constructor(name) { this.name = name; }
}
Object.assign(User.prototype, mixin);

const user = new User("Alice");
console.log(user.serialize()); // '{"name":"Alice"}'
const copy = user.clone();
console.log(copy.name); // "Alice"

// Object.assign does shallow copy
const deep = { nested: { a: 1 } };
const shallowCopy = Object.assign({}, deep);
shallowCopy.nested.a = 2;
console.log(deep.nested.a); // 2 — shared reference!
For deep cloning, use structuredClone() instead of Object.assign(). For prototype-aware copying, use Object.create(Object.getPrototypeOf(obj)) combined with Object.assign(). Object.assign() triggers setters on the target object, while Object.defineProperties() does not — this can lead to different behavior.

Why it matters: Understanding what Object.assign() does (and doesn't) copy clarifies why prototype methods added via assign become own properties, not inherited ones, and why shallow cloning an object with nested state can cause mutation bugs.

Real applications: Implementing mixin patterns (Object.assign to prototype), merging config defaults, creating shallow copies of state objects in Redux, and building utility functions that compose behavior from multiple sources into a class prototype.

Common mistakes: Thinking Object.assign() copies prototype methods (it only copies own enumerable properties), expecting nested objects to be independent after an Object.assign copy (they share references), and using Object.assign() when you need deep clone (use structuredClone instead).

Every function's prototype has a constructor property that points back to the function itself. Instances inherit this property, so you can use obj.constructor to identify what created an object. The constructor property is automatically set up when a function is declared, but it can be accidentally lost when you replace the entire prototype object — this is a common mistake. Understanding the constructor property helps with dynamic instance creation, type checking, and debugging. Here is how the constructor property works:
function Person(name) { this.name = name; }
const p = new Person("Alice");
console.log(p.constructor === Person); // true
console.log(p.constructor.name);       // "Person"

// Creating another instance dynamically
const p2 = new p.constructor("Bob");
console.log(p2.name); // "Bob"

// BUG: Replacing prototype loses constructor
function Animal(name) { this.name = name; }
Animal.prototype = {
  speak() { return this.name + " speaks"; }
};
const a = new Animal("Dog");
console.log(a.constructor === Animal); // false!
console.log(a.constructor === Object); // true — inherited from Object

// Fix: restore constructor
Animal.prototype = {
  constructor: Animal,  // restore it
  speak() { return this.name + " speaks"; }
};
Always restore the constructor property when replacing a prototype object, or use Object.assign() to add methods instead of replacing the entire prototype. ES6 classes handle this automatically — the constructor property is always correctly set on the class prototype.

Why it matters: The constructor property enables dynamic instance creation and type identification. Accidentally losing it (by replacing the prototype object) is a subtle bug that can break type-checking and dynamic instantiation patterns in libraries and frameworks.

Real applications: Dynamic factories that use new obj.constructor(...args) to create same-type clones, type identification utilities based on constructor.name, debugging object origins by reading constructor, and framework code that needs to re-instantiate objects from their type.

Common mistakes: Replacing the entire prototype object without restoring the constructor property (breaks obj.constructor checks), modifying constructor after ES6 class instances are created (the class syntax handles this correctly, manual prototype replacement doesn't), and using constructor.name as a reliable type identifier (minification can rename constructors to single characters).