Skip to content

原型与原型链:JavaScript 对象继承的底层机制

想象你在使用一个工具箱时,如果发现自己缺少某个工具,你会先看看箱子里有没有,如果没有,就会向邻居或者工具房借用。JavaScript 的对象系统也是这样工作的——每个对象都有自己的工具箱,当需要某个属性或方法时,先在自己的工具箱里找,找不到就沿着"邻里链"向上查找,直到找到或者到达链的末端。

这个巧妙的机制就是 JavaScript 的核心特性:原型与原型链

什么是原型:对象的家族谱系

在 JavaScript 中,每个对象都有一个隐藏的内部属性 [[Prototype]](在浏览器环境中,我们可以通过 __proto__ 访问它),这个属性指向另一个对象。这个被指向的对象就是当前对象的原型

javascript
// 创建一个简单的对象
const person = {
  name: "Sarah",
  age: 29,
  introduce() {
    console.log(`Hi, I'm ${this.name} and I'm ${this.age} years old`);
  },
};

// 查看对象的原型指向
console.log(person.__proto__ === Object.prototype); // true
console.log(Object.prototype.__proto__ === null); // true - 原型链的终点

即使是这个简单的 person 对象,也已经通过原型链继承了许多内置方法:

javascript
// person 对象自身没有 toString 方法
console.log("toString" in person); // true
console.log(person.hasOwnProperty("toString")); // false

// 但我们可以正常调用它,因为它在原型链上
console.log(person.toString()); // "[object Object]"

// hasOwnProperty 方法也是继承来的
console.log(person.hasOwnProperty("name")); // true - person 有自己的 name 属性
console.log(person.hasOwnProperty("toString")); // false - toString 在原型上

prototype 与 proto:函数与对象的双胞胎

理解 JavaScript 的原型系统,最关键的就是要区分 prototype__proto__ 这两个概念。它们虽然名字相似,但作用完全不同。

prototype 属性:函数的设计蓝图

prototype构造函数特有的属性,它定义了通过该函数创建的对象的原型。

javascript
function Vehicle(brand) {
  this.brand = brand;
  this.speed = 0;
}

// Vehicle.prototype 就是所有 Vehicle 实例的原型
console.log(Vehicle.prototype); // { constructor: f Vehicle() }

// 在原型上添加方法,所有实例都会继承
Vehicle.prototype.start = function () {
  console.log(`${this.brand} vehicle is starting...`);
  this.speed = 10;
};

Vehicle.prototype.accelerate = function (amount) {
  this.speed += amount;
  console.log(`${this.brand} is accelerating to ${this.speed} km/h`);
};

// 创建实例
const car = new Vehicle("Toyota");

// car 的原型指向 Vehicle.prototype
console.log(car.__proto__ === Vehicle.prototype); // true

proto 属性:对象的家谱链

__proto__对象特有的属性,它指向该对象的原型。

javascript
function Vehicle(brand) {
  this.brand = brand;
  this.speed = 0;
}

const car = new Vehicle("Toyota");

// car.__proto__ 指向 Vehicle.prototype
console.log(car.__proto__ === Vehicle.prototype); // true

// Vehicle.prototype.__proto__ 指向 Object.prototype
console.log(Vehicle.prototype.__proto__ === Object.prototype); // true

// Object.prototype.__proto__ 是 null,原型链的终点
console.log(Object.prototype.__proto__ === null); // true

记忆技巧

  • prototype 属于函数,是函数的"设计图"
  • __proto__ 属于对象,是对象的"出身证明"

原型链的查找机制:寻找宝藏的旅程

当你访问一个对象的属性时,JavaScript 引擎会执行一个精确的查找过程:

  1. 首先检查对象自身:查看对象是否有该属性
  2. 沿着原型链向上查找:如果没有,查看 __proto__ 指向的原型对象
  3. 继续向上追溯:重复步骤 2,直到找到属性或到达 null
javascript
function LivingBeing() {
  this.isAlive = true;
  this.bornTime = Date.now();
}

LivingBeing.prototype.breathe = function () {
  console.log(`${this.constructor.name} is breathing...`);
};

LivingBeing.prototype.getAge = function () {
  const age = Math.floor((Date.now() - this.bornTime) / 1000);
  return `${age} seconds old`;
};

function Animal(type, species) {
  LivingBeing.call(this);
  this.type = type;
  this.species = species;
}

// 建立原型链:Animal.prototype -> LivingBeing.prototype
Animal.prototype = Object.create(LivingBeing.prototype);
Animal.prototype.constructor = Animal;

Animal.prototype.speak = function () {
  console.log(`${this.species} makes a sound`);
};

Animal.prototype.move = function (speed) {
  console.log(`${this.species} moves at ${speed} km/h`);
};

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

// 建立原型链:Dog.prototype -> Animal.prototype -> LivingBeing.prototype
Dog.prototype = Object.create(Animal.prototype);
Dog.prototype.constructor = Dog;

Dog.prototype.bark = function () {
  console.log(`${this.name} barks: Woof! Woof!`);
};

Dog.prototype.wagTail = function () {
  console.log(`${this.name} wags tail happily`);
};

const myDog = new Dog("Max", "Golden Retriever");

// 让我们跟踪属性查找过程:
console.log(myDog.name); // "Max" - 在 myDog 对象上找到

// bark 方法的查找路径:
// 1. myDog.bark - 在 myDog 对象上查找 → 未找到
// 2. myDog.__proto__ (Dog.prototype).bark - 找到!
myDog.bark(); // "Max barks: Woof! Woof!"

// speak 方法的查找路径:
// 1. myDog.speak - 在 myDog 对象上查找 → 未找到
// 2. Dog.prototype.speak - 在 Dog.prototype 上查找 → 未找到
// 3. Animal.prototype.speak - 在 Animal.prototype 上查找 → 找到!
myDog.speak(); // "dog makes a sound"

// breathe 方法的查找路径:
// 1. myDog.breathe - 未找到
// 2. Dog.prototype.breathe - 未找到
// 3. Animal.prototype.breathe - 未找到
// 4. LivingBeing.prototype.breathe - 找到!
myDog.breathe(); // "Dog is breathing..."

// getAge 方法的查找路径:
// 1. myDog.getAge - 未找到
// 2. Dog.prototype.getAge - 未找到
// 3. Animal.prototype.getAge - 未找到
// 4. LivingBeing.prototype.getAge - 找到!
console.log(myDog.getAge()); // "X seconds old"

// toString 方法的查找路径:
// 一直向上到 Object.prototype 才找到
console.log(myDog.toString()); // "[object Object]"

这个查找机制就像你在图书馆找一本书的过程:先在自己的书架上找,没有就去班级图书角,没有就去学校图书馆,最终可能在市图书馆找到。

原型链的实际应用场景

1. 方法共享:节省内存的智慧

原型链最常见的用途是让多个对象共享方法,这样可以节省大量内存:

javascript
function Smartphone(brand, model, storage) {
  this.brand = brand;
  this.model = model;
  this.storage = storage;
  this.apps = [];
}

// 将方法放在原型上,所有实例共享
Smartphone.prototype.powerOn = function () {
  console.log(`${this.brand} ${this.model} is booting up...`);
};

Smartphone.prototype.installApp = function (appName, size) {
  if (this.storage >= size) {
    this.apps.push({ name: appName, size: size });
    this.storage -= size;
    console.log(`${appName} installed successfully`);
  } else {
    console.log(`Not enough storage. Need ${size}MB, have ${this.storage}MB`);
  }
};

Smartphone.prototype.listApps = function () {
  console.log(`Installed apps on ${this.brand} ${this.model}:`);
  this.apps.forEach((app) => console.log(`- ${app.name} (${app.size}MB)`));
};

Smartphone.prototype.uninstallApp = function (appName) {
  const appIndex = this.apps.findIndex((app) => app.name === appName);
  if (appIndex !== -1) {
    const app = this.apps[appIndex];
    this.apps.splice(appIndex, 1);
    this.storage += app.size;
    console.log(`${appName} uninstalled successfully`);
  } else {
    console.log(`${appName} not found`);
  }
};

// 创建多个实例
const iPhone = new Smartphone("Apple", "iPhone 15", 128);
const galaxy = new Smartphone("Samsung", "Galaxy S24", 256);

// 所有实例共享相同的方法
iPhone.powerOn(); // "Apple iPhone 15 is booting up..."
galaxy.powerOn(); // "Samsung Galaxy S24 is booting up..."

// 使用共享的方法
iPhone.installApp("WhatsApp", 150);
iPhone.installApp("Instagram", 200);
galaxy.installApp("TikTok", 300);

iPhone.listApps();
galaxy.listApps();

// 验证方法确实是共享的
console.log(iPhone.powerOn === galaxy.powerOn); // true
console.log(iPhone.installApp === galaxy.installApp); // true

// 内存效率:如果有10000个实例,只需要一个方法副本
// 而不是每个实例都有自己的方法副本

2. 属性覆盖:个性与共性的平衡

子对象可以覆盖原型上的属性,同时还能通过原型链访问父级的属性:

javascript
function Employee(name, position) {
  this.name = name;
  this.position = position;
  this.workingHours = 40;
}

Employee.prototype.department = "General";
Employee.prototype.salary = 50000;
Employee.prototype.getJobInfo = function () {
  return `${this.position} in ${this.department} department`;
};

Employee.prototype.requestLeave = function (days) {
  console.log(`${this.name} is requesting ${days} days leave`);
};

function Manager(name, position, teamSize) {
  Employee.call(this, name, position);
  this.teamSize = teamSize;
}

Manager.prototype = Object.create(Employee.prototype);
Manager.prototype.constructor = Manager;

// 覆盖原型上的属性
Manager.prototype.department = "Management";
Manager.prototype.salary = 80000;
Manager.prototype.workingHours = 45;

// 添加管理器特有方法
Manager.prototype.approveLeave = function (employee, days) {
  console.log(
    `Manager ${this.name} approved ${employee.name}'s ${days} days leave`
  );
};

Manager.prototype.runTeamMeeting = function () {
  console.log(
    `${this.name} is conducting a team meeting for ${this.teamSize} team members`
  );
};

const employee1 = new Employee("John Doe", "Developer");
const manager1 = new Manager("Jane Smith", "Senior Manager", 8);

console.log(employee1.getJobInfo()); // "Developer in General department"
console.log(manager1.getJobInfo()); // "Senior Manager in Management department"

console.log(employee1.salary); // 50000 (来自原型)
console.log(manager1.salary); // 80000 (覆盖了原型的值)

// 仍然可以使用原型上的方法
employee1.requestLeave(5); // "John Doe is requesting 5 days leave"
manager1.requestLeave(3); // "Jane Smith is requesting 3 days leave"

// 管理器特有的方法
manager1.approveLeave(employee1, 5); // "Manager Jane Smith approved John Doe's 5 days leave"
manager1.runTeamMeeting(); // "Jane Smith is conducting a team meeting for 8 team members"

3. 检查属性来源:辨别自有与继承

使用 hasOwnProperty 方法可以区分属性是对象自身的还是继承的:

javascript
function Task(title, priority) {
  this.title = title;
  this.priority = priority;
  this.completed = false;
}

Task.prototype.category = "General";
Task.prototype.description = "Default task description";

Task.prototype.toggleComplete = function () {
  this.completed = !this.completed;
  console.log(
    `Task "${this.title}" marked as ${this.completed ? "completed" : "pending"}`
  );
};

Task.prototype.getDetails = function () {
  return `${this.title} (${this.priority} priority) - ${
    this.completed ? "Done" : "Pending"
  }`;
};

const task1 = new Task("Write report", "High");

// 检查属性来源
console.log(task1.hasOwnProperty("title")); // true (自有属性)
console.log(task1.hasOwnProperty("priority")); // true (自有属性)
console.log(task1.hasOwnProperty("completed")); // true (自有属性)
console.log(task1.hasOwnProperty("category")); // false (继承属性)
console.log(task1.hasOwnProperty("description")); // false (继承属性)
console.log(task1.hasOwnProperty("toggleComplete")); // false (继承方法)

// in 操作符会查找整个原型链
console.log("title" in task1); // true
console.log("category" in task1); // true
console.log("toggleComplete" in task1); // true

// 自定义函数判断属性类型
function getPropertyInfo(obj, prop) {
  const hasOwn = obj.hasOwnProperty(prop);
  const inProto = prop in obj;

  if (hasOwn) {
    return `${prop}: 自有属性`;
  } else if (inProto) {
    return `${prop}: 继承属性`;
  } else {
    return `${prop}: 不存在`;
  }
}

console.log(getPropertyInfo(task1, "title")); // "title: 自有属性"
console.log(getPropertyInfo(task1, "category")); // "category: 继承属性"
console.log(getPropertyInfo(task1, "nonexistent")); // "nonexistent: 不存在"

原型链的高级操作

1. 动态修改原型:实时添加能力

javascript
function Calculator() {
  this.result = 0;
}

Calculator.prototype.add = function (number) {
  this.result += number;
  return this;
};

Calculator.prototype.subtract = function (number) {
  this.result -= number;
  return this;
};

Calculator.prototype.getResult = function () {
  return this.result;
};

const calc = new Calculator();
calc.add(10).subtract(3).add(5);
console.log(calc.getResult()); // 12

// 动态添加新方法到原型
Calculator.prototype.multiply = function (number) {
  this.result *= number;
  return this;
};

Calculator.prototype.divide = function (number) {
  if (number !== 0) {
    this.result /= number;
  } else {
    console.log("Cannot divide by zero");
  }
  return this;
};

calc.multiply(2).divide(4);
console.log(calc.getResult()); // 4.5

// 现有的计算器实例也能使用新方法
const calc2 = new Calculator();
calc2.add(20).multiply(3);
console.log(calc2.getResult()); // 60

2. 原型链检测:理解对象关系

javascript
// 获取对象的原型链
function getPrototypeChain(obj) {
  const chain = [];
  let current = obj;

  while (current !== null) {
    chain.push(current.constructor.name || "Object");
    current = Object.getPrototypeOf(current);
  }

  return chain.join(" -> ");
}

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

function Circle(radius) {
  Shape.call(this, "Circle");
  this.radius = radius;
}

Circle.prototype = Object.create(Shape.prototype);
Circle.prototype.constructor = Circle;

function ColoredCircle(radius, color) {
  Circle.call(this, radius);
  this.color = color;
}

ColoredCircle.prototype = Object.create(Circle.prototype);
ColoredCircle.prototype.constructor = ColoredCircle;

const coloredCircle = new ColoredCircle(5, "red");
console.log(getPrototypeChain(coloredCircle)); // "ColoredCircle -> Circle -> Shape -> Object"

// 检查对象是否是某个构造函数的实例
console.log(coloredCircle instanceof ColoredCircle); // true
console.log(coloredCircle instanceof Circle); // true
console.log(coloredCircle instanceof Shape); // true
console.log(coloredCircle instanceof Array); // false

// 更精确的原型检查
function isPrototypeOf(Constructor, obj) {
  return obj.constructor === Constructor;
}

console.log(isPrototypeOf(ColoredCircle, coloredCircle)); // true
console.log(isPrototypeOf(Circle, coloredCircle)); // false

原型链的性能考虑

1. 避免过深的原型链

过深的原型链会影响属性查找的性能,应该保持合理的深度:

javascript
// ❌ 不推荐:过深的原型链
function Level1() {}
function Level2() {}
function Level3() {}
function Level4() {}
function Level5() {}
function Level6() {}

Level2.prototype = Object.create(Level1.prototype);
Level3.prototype = Object.create(Level2.prototype);
Level4.prototype = Object.create(Level3.prototype);
Level5.prototype = Object.create(Level4.prototype);
Level6.prototype = Object.create(Level5.prototype);

const deepObj = new Level6();
// 查找深层属性需要遍历整个原型链,性能较差

// ✅ 推荐:合理的原型链深度(通常不超过3-4层)
function BaseClass() {}
function MiddleClass() {}
function TopClass() {}

MiddleClass.prototype = Object.create(BaseClass.prototype);
TopClass.prototype = Object.create(MiddleClass.prototype);

const reasonableObj = new TopClass();
// 查找属性只需要遍历较少的原型链

2. 原型链污染风险

javascript
// ❌ 危险:修改 Object.prototype
// 这会影响到所有对象,可能导致意外的行为
Object.prototype.someDangerousMethod = function () {
  console.log("This method affects all objects!");
};

const obj = { a: 1 };
obj.someDangerousMethod(); // 现在所有对象都有了这个方法

// ✅ 安全:创建自己的工具对象
const StringUtils = {
  capitalize(str) {
    return str.charAt(0).toUpperCase() + str.slice(1);
  },

  reverse(str) {
    return str.split("").reverse().join("");
  },

  truncate(str, length) {
    return str.length > length ? str.slice(0, length) + "..." : str;
  },
};

// 使用工具对象,而不是污染全局原型
const text = "hello world";
console.log(StringUtils.capitalize(text)); // "Hello world"

3. 修改原型的时机

javascript
function Person(name) {
  this.name = name;
}

const john = new Person("John");

// 在创建实例后修改原型
Person.prototype.sayHello = function () {
  console.log(`Hello, I'm ${this.name}`);
};

// john 对象可以访问到新添加的方法
// 因为 JavaScript 会动态查找原型链
john.sayHello(); // "Hello, I'm John"

// 但更推荐的做法是在创建实例前完成所有原型设置

现代 JavaScript 中的替代方案

虽然原型链是 JavaScript 的核心机制,但在现代 JavaScript 开发中,我们通常使用更高级的语法。

1. Class 语法:原型的现代包装

javascript
// 现代的 Class 语法(底层仍然是原型链)
class Animal {
  constructor(type) {
    this.type = type;
  }

  speak() {
    console.log(`${this.type} makes a sound`);
  }

  move(speed) {
    console.log(`${this.type} moves at ${speed} km/h`);
  }
}

class Dog extends Animal {
  constructor(name, breed) {
    super("dog"); // 调用父类构造函数
    this.name = name;
    this.breed = breed;
  }

  bark() {
    console.log(`${this.name} (${this.breed}) barks: Woof!`);
  }

  // 重写父类方法
  speak() {
    console.log(`${this.name} barks loudly`);
  }

  // 调用父类方法
  getDetails() {
    return `${this.name} is a ${this.breed} dog`;
  }
}

const myDog = new Dog("Max", "Golden Retriever");
myDog.speak(); // 重写的方法
myDog.move(15); // 继承的方法
myDog.bark(); // 自有的方法

console.log(myDog.getDetails()); // "Max is a Golden Retriever dog"

2. Object.create():直接创建原型对象

javascript
// 创建具有指定原型的对象
const animalProto = {
  type: "unknown",

  speak() {
    console.log(`${this.type} makes a sound`);
  },

  move(speed) {
    console.log(`${this.type} moves at ${speed} km/h`);
  },
};

const cat = Object.create(animalProto);
cat.type = "cat";
cat.meow = function () {
  console.log("Cat meows: Meow!");
};

cat.speak(); // 继承的方法
cat.meow(); // 自有的方法

// 创建更复杂的原型链
const mammalProto = Object.create(animalProto, {
  type: { value: "mammal" },
  warmBlooded: { value: true },
});

mammalProto.giveBirth = function () {
  console.log(`${this.type} gives birth to live young`);
};

const dog = Object.create(mammalProto);
dog.type = "dog";
dog.bark = function () {
  console.log("Dog barks: Woof!");
};

dog.speak(); // "dog makes a sound" (来自 animalProto)
dog.giveBirth(); // "dog gives birth to live young" (来自 mammalProto)
dog.bark(); // "Dog barks: Woof!" (自有方法)

3. Object.setPrototypeOf():动态设置原型

javascript
const baseObject = {
  commonMethod() {
    console.log("This is a common method");
  },
};

const extendedObject = {
  specificMethod() {
    console.log("This is a specific method");
  },
};

// 动态设置原型
Object.setPrototypeOf(extendedObject, baseObject);

extendedObject.commonMethod(); // 可以访问基对象的方法
extendedObject.specificMethod(); // 可以访问自己的方法

// 验证原型关系
console.log(extendedObject.__proto__ === baseObject); // true

总结:掌握原型的力量

原型与原型链是 JavaScript 实现对象间属性和方法共享的基础机制。理解它们的工作原理对于深入掌握 JavaScript 至关重要:

核心概念回顾

  1. 每个对象都有一个原型:通过 __proto__ 属性访问
  2. 每个函数都有一个 prototype 属性:决定了其创建实例的原型
  3. 属性查找沿着原型链进行:直到找到或到达链尾(null)
  4. 方法应该放在原型上:以实现内存共享和代码复用
  5. 合理使用原型链:可以构建高效的继承体系

实际应用建议

  • 方法共享:将通用方法放在原型上,让所有实例共享
  • 属性覆盖:子对象可以覆盖原型属性,同时保留访问父属性的能力
  • 性能优化:避免过深的原型链,保持合理的继承层次
  • 安全编程:避免污染全局原型,创建专门的工具对象

虽然现代 JavaScript 提供了 Class 等更友好的语法,但底层仍然是基于原型链机制。理解原型不仅能帮助你更好地使用 JavaScript,还能在遇到复杂问题时找到解决方案,编写出更加高效和优雅的代码。

原型链是 JavaScript 的精髓,掌握它就意味着真正理解了这门语言的本质。在 JavaScript 的世界里,没有孤立的对象,每个对象都活在原型链这个大家族中。