正在加载,请稍候…

JavaScript Prototype Chain Explained: Inheritance Without Classes

Master JavaScript prototypal inheritance. Learn how the prototype chain works, the difference between __proto__ and prototype, class syntax sugar, and common patterns.

JavaScript Prototype Chain Explained: Inheritance Without Classes

JavaScript's inheritance model is fundamentally different from classical OOP languages like Java. Understanding prototypes unlocks how JavaScript actually works — and explains a lot of "surprising" behavior.

Every Object Has a Prototype

const dog = { name: 'Rex', sound: 'Woof' };

// dog's prototype is Object.prototype
console.log(Object.getPrototypeOf(dog) === Object.prototype); // true

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

The Prototype Chain

When you access a property, JavaScript walks up the chain:

const animal = {
  breathe() { return 'breathing...'; },
  eat() { return 'eating...'; }
};

const dog = {
  bark() { return 'Woof!'; }
};

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

// dog can now use animal's methods
console.log(dog.bark());    // 'Woof!' (own property)
console.log(dog.breathe()); // 'breathing...' (from prototype)
console.log(dog.eat());     // 'eating...' (from prototype)

// Property lookup chain:
// dog.breathe → not found on dog → check dog's prototype (animal) → found!

__proto__ vs prototype

This confuses everyone. Here's the clear distinction:

// __proto__ — the actual prototype link on every object instance
const obj = {};
console.log(obj.__proto__ === Object.prototype); // true

// .prototype — exists only on FUNCTIONS, used as template for new instances
function Dog(name) {
  this.name = name;
}
Dog.prototype.bark = function() { return 'Woof!'; };

const rex = new Dog('Rex');
console.log(rex.__proto__ === Dog.prototype); // true ← key relationship!
                Dog (constructor function)
                 │
                 │ .prototype
                 ↓
            Dog.prototype ←─── rex.__proto__
            { bark: fn }        (same object)
                 │
                 │ __proto__
                 ↓
           Object.prototype
           { toString, hasOwnProperty, ... }
                 │
                 │ __proto__
                 ↓
               null

The new Keyword

function Person(name, age) {
  this.name = name;
  this.age = age;
}
Person.prototype.greet = function() {
  return `Hi, I'm ${this.name}`;
};

const alice = new Person('Alice', 30);
// new does 4 things:
// 1. Creates empty object: {}
// 2. Sets __proto__: {}.__proto__ = Person.prototype
// 3. Calls Person with this = the new object
// 4. Returns the object (unless constructor returns different object)

console.log(alice.name);    // 'Alice' (own property)
console.log(alice.greet()); // 'Hi, I'm Alice' (from prototype)
console.log(alice instanceof Person); // true

Class Syntax: Sugar Over Prototypes

ES6 classes are syntactic sugar — they use the same prototype system underneath.

// ES5 prototype-based
function Animal(name) {
  this.name = name;
}
Animal.prototype.speak = function() {
  return `${this.name} makes a noise.`;
};

function Dog(name) {
  Animal.call(this, name); // Call parent constructor
}
Dog.prototype = Object.create(Animal.prototype); // Set up inheritance
Dog.prototype.constructor = Dog; // Fix constructor reference
Dog.prototype.bark = function() {
  return `${this.name} barks.`;
};

// ES6 class (identical behavior)
class Animal {
  constructor(name) {
    this.name = name;
  }
  speak() {
    return `${this.name} makes a noise.`;
  }
}

class Dog extends Animal {
  bark() {
    return `${this.name} barks.`;
  }
}

const d = new Dog('Rex');
console.log(d.speak()); // Rex makes a noise. (from Animal.prototype)
console.log(d.bark());  // Rex barks. (from Dog.prototype)

Object.create() — Explicit Prototype Inheritance

const vehicleProto = {
  start() { return `${this.model} starting...`; },
  stop()  { return `${this.model} stopping.`; },
};

// Create object with vehicleProto as its prototype
const car = Object.create(vehicleProto);
car.model = 'Tesla Model 3';
car.drive = function() { return `Driving ${this.model}`; };

console.log(car.start()); // Tesla Model 3 starting... (from proto)
console.log(car.drive()); // Driving Tesla Model 3 (own method)

// Object.create(null) — no prototype (useful for pure dictionaries)
const pureDict = Object.create(null);
pureDict.key = 'value';
// No toString, no hasOwnProperty — truly clean object

Checking Own vs Inherited Properties

class Animal {
  constructor(name) { this.name = name; }
  speak() { return 'some noise'; }
}
class Dog extends Animal {
  bark() { return 'Woof!'; }
}

const rex = new Dog('Rex');

// hasOwnProperty — only own (not inherited) properties
console.log(rex.hasOwnProperty('name'));  // true (set in constructor)
console.log(rex.hasOwnProperty('bark'));  // false (on Dog.prototype)
console.log(rex.hasOwnProperty('speak')); // false (on Animal.prototype)

// in operator — checks entire chain
console.log('name' in rex);   // true
console.log('bark' in rex);   // true
console.log('speak' in rex);  // true
console.log('fly' in rex);    // false

// instanceof — checks prototype chain
console.log(rex instanceof Dog);    // true
console.log(rex instanceof Animal); // true
console.log(rex instanceof Object); // true (everything is!)

Mixin Pattern (Composing Behaviors)

// Mixins let you compose behaviors without deep inheritance chains
const Serializable = {
  serialize() {
    return JSON.stringify(this);
  },
  static deserialize(json) {
    return Object.assign(new this(), JSON.parse(json));
  }
};

const Validatable = {
  validate() {
    return Object.keys(this).every(key => this[key] !== null);
  }
};

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

// Mix in behaviors
Object.assign(User.prototype, Serializable, Validatable);

const user = new User('Alice', 'alice@example.com');
console.log(user.serialize());  // '{"name":"Alice","email":"alice@example.com"}'
console.log(user.validate());   // true

Common Gotchas

// ❌ Gotcha: Shared mutable state on prototype
function Team() {}
Team.prototype.members = []; // Shared across ALL instances!

const t1 = new Team();
const t2 = new Team();
t1.members.push('Alice');
console.log(t2.members); // ['Alice'] — oops!

// ✅ Fix: Initialize in constructor
function Team() {
  this.members = []; // Own property per instance
}

// ❌ Gotcha: Arrow functions can't be constructors
const Foo = () => {};
new Foo(); // TypeError: Foo is not a constructor

// ❌ Gotcha: Arrow functions don't have their own prototype
const bar = () => {};
console.log(bar.prototype); // undefined

Summary

  • Every JS object has a prototype (except Object.create(null))
  • Property lookup walks up the chain until found or reaches null
  • __proto__: actual prototype link on instances
  • .prototype: exists on functions, becomes __proto__ of new instances
  • ES6 classes are syntax sugar — same prototype system underneath
  • Prefer composition (mixins) over deep inheritance hierarchies

→ Inspect nested JSON/object structures with the JSON Viewer.