【前端三剑客-25/Lesson43(2025-11-25)】JavaScript 原型系统:从构造函数到原型链的全面解析🚗

29 阅读5分钟

🚗JavaScript 是一门独特而强大的编程语言,其面向对象机制与其他主流语言(如 Java、C++)有着根本性的不同。它不依赖“类”来定义对象结构,而是采用基于原型(Prototype-based)的继承模型。这种设计源于 Self 语言,赋予了 JavaScript 极高的灵活性和动态性。本文将深入剖析 JavaScript 的原型系统,涵盖从基础概念到高级应用的全部内容,帮助你彻底掌握这一核心机制。


🔧 构造函数与实例:对象创建的起点

在 ES6 引入 class 语法之前,JavaScript 中没有传统意义上的“类”。开发者通过构造函数(Constructor Function)来模拟类的行为:

function Car(color) {
  this.color = color; // 实例属性,每个实例独有
}

当使用 new 关键字调用构造函数时,JavaScript 引擎会执行以下步骤:

  1. 创建一个全新的空对象 {}
  2. 将该对象的内部 [[Prototype]] 链接(即 __proto__)指向 Car.prototype
  3. 执行构造函数体,其中的 this 指向新创建的对象;
  4. 如果构造函数没有显式返回对象,则自动返回这个新对象。

因此,const car1 = new Car('霞光紫') 会生成一个具有 color: '霞光紫' 属性的对象,并且该对象能访问 Car.prototype 上定义的所有属性和方法。

💡 关键点:构造函数本身只是一个普通函数,只有通过 new 调用时才具备“构造”语义。


🧬 prototype:共享行为的容器

每个函数(包括构造函数)都自带一个名为 prototype 的属性,它是一个普通对象。这个对象的作用是:存放所有实例共享的属性和方法

Car.prototype = {
  drive() {
    console.log('drive,下赛道');
  },
  name: 'su7',
  height: 1.4,
  weight: 1.5,
  long: 4800
};

这样,无论创建多少个 Car 实例(如 car1, car2),它们都共享同一个 drive 方法和 name 等属性,从而节省内存,避免重复创建相同的方法。

最佳实践:将实例特有数据(如 color)放在构造函数中,将共享行为/数据放在 prototype 上。


🔗 __proto__ 与 [[Prototype]]:原型链接的本质

每个对象(除了 Object.create(null) 创建的对象)都有一个内部属性 [[Prototype]],它指向该对象的原型对象。虽然标准中不可直接访问 [[Prototype]],但大多数引擎提供了 __proto__ 作为非标准访问方式(不推荐使用)。

更规范的方式是使用:

  • Object.getPrototypeOf(obj):获取对象的原型;
  • Object.setPrototypeOf(obj, proto):设置对象的原型(性能差,慎用)。

例如:

const person1 = new Person('张三', 18);
console.log(person1.__proto__ === Person.prototype); // true
console.log(Object.getPrototypeOf(person1) === Person.prototype); // true

这表明:实例的 [[Prototype]] 指向其构造函数的 prototype 对象


📜 原型链:属性查找的路径

当访问一个对象的属性(如 obj.prop)时,JavaScript 引擎会按以下顺序查找:

  1. 首先检查对象自身是否有该属性
  2. 如果没有,则沿着 [[Prototype]] 链向上查找;
  3. 重复此过程,直到找到属性或到达链的末端(即 null)。

这个查找路径就称为原型链(Prototype Chain)。

例如:

function Animal() {}
Animal.prototype.species = '动物';

function Dog() {}
Dog.prototype = new Animal(); // 建立原型链

const myDog = new Dog();
console.log(myDog.species); // '动物' —— 通过原型链查到

最终,所有对象的原型链都会终止于 Object.prototype,而 Object.prototype.__proto__ === null,表示链的终点。

⚠️ 注意:如果在实例上设置了同名属性(如 myDog.species = '哺乳动物'),则会遮蔽(shadow)原型上的属性,后续访问将不再沿链查找。


🏛️ constructor:构造函数的回指

每个 prototype 对象默认都有一个 constructor 属性,指向其关联的构造函数:

Person.prototype.constructor === Person; // true

但当你重写整个 prototype 对象时(如 Person.prototype = { ... }),原有的 constructor 会被覆盖,导致:

Person.prototype.constructor === Object; // ❌ 错误!

因此,重写 prototype 时应手动恢复 constructor

Person.prototype = {
  constructor: Person, // ✅ 手动设置
  sayHi() { /*...*/ }
};

否则,通过 instance.constructor 获取构造函数将出错。


🔄 继承:组合继承与 Object.create

JavaScript 支持多种继承模式,其中组合继承(Combination Inheritance)最为常用:

// 父类
function Parent(name) {
  this.name = name;
}
Parent.prototype.sayName = function() {
  console.log(this.name);
};

// 子类
function Child(name, age) {
  Parent.call(this, name); // 借用构造函数,继承实例属性
  this.age = age;
}

// 原型链继承方法
Child.prototype = Object.create(Parent.prototype);
Child.prototype.constructor = Child;

Child.prototype.sayAge = function() {
  console.log(this.age);
};

这里的关键是 Object.create(Parent.prototype):它创建一个Parent.prototype 为原型的新对象,避免了直接调用 new Parent() 可能带来的副作用(如父类构造函数执行不必要的初始化)。

Object.create(proto) 是构建原型链的标准方式,比 new 更精准。


🧪 toString 从何而来?—— 内置原型链的体现

你可能会疑惑:为什么一个空对象 {} 也能调用 .toString()

答案就在原型链的顶端:Object.prototype

({}).toString(); // "[object Object]"

因为:

  • {}[[Prototype]] 指向 Object.prototype
  • Object.prototype 上定义了 toString, valueOf, hasOwnProperty 等方法;
  • 所有对象(除非显式断开)都能通过原型链访问这些方法。

这就是为何即使你没定义 toString,它依然存在。


🧱 ES6 class:原型系统的语法糖

ES6 引入的 class 并非真正的“类”,而是对原型模式的语法封装

class Person {
  constructor(name, age) {
    this.name = name;
    this.age = age;
  }
  sayHi() {
    console.log(`你好,我是${this.name}`);
  }
}

上述代码在运行时等价于:

function Person(name, age) {
  this.name = name;
  this.age = age;
}
Person.prototype.sayHi = function() { /*...*/ };

class 提供了更清晰的结构和静态方法、继承(extends)等便利语法,但底层仍是原型链。

📌 重要认知:理解 class 背后的原型机制,才能真正掌握 JavaScript 面向对象的本质。


⚠️ 常见误区与陷阱

1. 误以为实例有 prototype 属性

console.log(car1.prototype); // undefined ❌

只有函数才有 prototype;实例只有 __proto__(或通过 Object.getPrototypeOf 访问原型)。

2. 在原型上使用引用类型属性

Person.prototype.skills = ['JS'];
person1.skills.push('CSS');
console.log(person2.skills); // ['JS', 'CSS'] ❌ 被意外修改!

所有实例共享同一个数组。应避免在原型上存储可变的引用类型数据。

3. 滥用 __proto__

虽然可用,但它是非标准属性。现代代码应使用 Object.getPrototypeOf / Object.setPrototypeOf


🚀 高级应用:扩展内置对象(谨慎!)

JavaScript 允许你扩展内置对象的原型:

Array.prototype.last = function() {
  return this[this.length - 1];
};
[1, 2, 3].last(); // 3

但《JavaScript 语言精粹》强烈警告:不要随意修改内置原型,因为:

  • 可能与未来标准冲突;
  • 可能破坏第三方库;
  • 在多人协作中引发不可预知的 bug。

仅在严格控制的环境下(如 polyfill)才考虑此类操作。


⚡ 性能考量

  • 原型链越深,属性查找越慢。频繁访问的属性应直接定义在实例上。
  • 缓存原型方法引用可提升性能:
    const toString = Object.prototype.toString;
    function isArray(obj) {
      return toString.call(obj) === '[object Array]';
    }
    
  • 避免动态修改原型链(如 Object.setPrototypeOf),这会破坏 JavaScript 引擎的优化假设,严重影响性能。

🌟 哲学之美:基于原型 vs 基于类

传统“基于类”的语言强调抽象模板(类)与具体实例的关系,是一种血缘继承

而 JavaScript 的“基于原型”则是对象直接继承对象,通过委托(Delegation)实现复用。正如《你不知道的 JS》所说:

“JavaScript 中的对象就像一张蓝图的复制品,而不是某个抽象类的实例。”

这种模式更灵活:你可以随时修改原型,所有实例立即获得新行为;也可以让对象直接从任意对象继承,无需预先定义“类”。


🧩 综合实战:完整的原型继承体系

以下是一个完整的示例,融合了本文所有核心概念:

// 基础类
function Animal(name) {
  this.name = name;
  this.alive = true;
}
Animal.prototype = {
  constructor: Animal,
  eat() { console.log(`${this.name} 在进食`); },
  sleep() { console.log(`${this.name} 在睡觉`); }
};

// 派生类
function Dog(name, breed) {
  Animal.call(this, name); // 继承实例属性
  this.breed = breed;
}
Dog.prototype = Object.create(Animal.prototype);
Dog.prototype.constructor = Dog;
Dog.prototype.bark = function() {
  console.log(`${this.name} 汪汪叫`);
};

// 使用
const myDog = new Dog('小黑', '拉布拉多');
myDog.eat();  // 继承自 Animal
myDog.bark(); // Dog 自有方法

// 验证关系
console.log(myDog instanceof Dog);     // true
console.log(myDog instanceof Animal);  // true
console.log(Object.getPrototypeOf(myDog) === Dog.prototype); // true
console.log(Object.getPrototypeOf(Dog.prototype) === Animal.prototype); // true

✅ 总结:掌握原型,掌握 JavaScript 的灵魂

JavaScript 的原型系统是其最强大、最优雅的特性之一。它通过简单的对象链接机制,实现了灵活、动态的继承模型。理解以下核心要点至关重要:

  • 构造函数 + prototype = 模拟“类”;
  • 实例通过 [[Prototype]] 链接原型;
  • 属性查找沿原型链进行;
  • class 只是语法糖,本质仍是原型;
  • 避免常见陷阱,遵循最佳实践;
  • 原型体现的是行为委托,而非类继承。

正如《JavaScript 语言精粹》所言:“原型继承是 JavaScript 最具表现力的特性之一。”

深入掌握原型系统,不仅能写出更高效、更清晰的代码,还能在面试中脱颖而出,真正成为 JavaScript 高手。🚀