JavaScript 原型系统深度解析:从构造函数到 `__proto__` 的完整链路(上卷)

49 阅读5分钟

前言

在前端开发的世界里,JavaScript 的原型(Prototype)机制既是其最强大的特性之一,也是初学者最容易陷入困惑的“迷宫”。许多开发者能写出功能代码,却对“为什么实例能调用原型方法?”、“prototype__proto__ 到底有何区别?”等问题语焉不详。这种模糊认知在面对复杂框架源码或大厂面试时,往往成为瓶颈。

本文将彻底拆解 JavaScript 原型系统的底层逻辑,聚焦 构造函数、prototype__proto__constructor 四大核心概念,通过精准定义、代码验证与图示类比,帮助你构建清晰、稳固的认知模型。无论你是刚入门的新手,还是希望查漏补缺的资深工程师,本文都将为你打开一扇通往 JS 面向对象本质的大门。


一、函数是一等对象:prototype 的诞生前提

JavaScript 中有一个根本原则:函数是对象。这意味着函数不仅可以被调用,还可以拥有属性和方法。正是基于这一特性,JavaScript 引擎在创建每一个函数时,自动为其附加一个名为 prototype 的属性

js
编辑
function Car(color) {
  this.color = color;
}
console.log(Car.prototype); // { constructor: Car }

这个 Car.prototype 是一个普通的 JavaScript 对象,它的特殊之处在于:Car 被用作构造函数(即通过 new Car() 调用)时,所有新创建的实例都会以 Car.prototype 作为其原型

✅ 核心规则:

  • 只有函数才有 prototype 属性
  • 普通对象(如字面量 {}、数组 []、日期 new Date()没有 prototype 属性,访问会返回 undefined

这一点常被误解。例如:

js
编辑
const obj = {};
console.log(obj.prototype); // undefined ❌

obj 是对象,不是函数,自然没有 prototype


二、实例没有 prototype,但有 __proto__:原型链的起点

当我们执行 const su7 = new Car('霞光紫') 时,JavaScript 引擎会完成以下关键步骤:

  1. 创建一个全新的空对象:{}
  2. 将该对象的内部隐式属性 [[Prototype]] 设置为 Car.prototype
  3. 将 Car 函数中的 this 绑定到这个新对象,并执行函数体
  4. 如果函数没有显式返回对象,则默认返回这个新对象

其中第 2 步是原型机制的核心。虽然 [[Prototype]] 是内部属性,但几乎所有浏览器都提供了非标准但广泛支持的访问方式:__proto__

因此:

js
编辑
const su7 = new Car('霞光紫');
console.log(su7.prototype);        // undefined(实例无此属性)
console.log(su7.__proto__);        // { constructor: Car, ... }
console.log(su7.__proto__ === Car.prototype); // true ✅

📌 结论
实例对象本身不拥有 prototype,但它通过 __proto__ 链接到其构造函数的 prototype 对象,从而实现属性和方法的继承与共享。


三、所有实例共享同一个原型对象:内存与设计的双赢

无论你创建多少个 Car 实例,它们的 __proto__ 都指向同一个 Car.prototype 对象

js
编辑
const car1 = new Car('红');
const car2 = new Car('蓝');

console.log(car1.__proto__ === car2.__proto__); // true
console.log(car1.__proto__ === Car.prototype);   // true

这意味着:

  • 在 Car.prototype 上定义的方法(如 drive()),所有实例均可调用,且只存储一份,极大节省内存
  • 动态修改 Car.prototype(如添加新方法),所有已有实例立即生效,体现了 JavaScript 的动态性
js
编辑
Car.prototype.honk = function() { console.log('嘀嘀!'); };
car1.honk(); // '嘀嘀!' —— 即使 car1 在 honk 定义前创建

💡 这正是事件委托、UI 组件库、工具函数封装等高级模式的底层支撑。


四、constructor:原型上的“回指针”及其陷阱

每个函数的 prototype 对象默认包含一个 constructor 属性,指向该函数本身

js
编辑
function Person(name) { this.name = name; }
console.log(Person.prototype.constructor === Person); // true

这个设计使得实例能够“反向追溯”自己的构造函数:

js
编辑
const p = new Person('张三');
console.log(p.constructor === Person); // true

⚠️ 经典陷阱:重写 prototype 会丢失 constructor

当你使用对象字面量完全覆盖原型时:

js
编辑
Person.prototype = {
  sayHi() { console.log('Hi'); }
  // 注意:这里没有 constructor!
};

此时,Person.prototype 变成了一个普通对象,其 constructor 默认继承自 Object.prototype,即指向 Object

js
编辑
const p = new Person('李四');
console.log(p.constructor === Person); // false!
console.log(p.constructor === Object); // true ❌

这会导致类型判断错误、工厂函数失效等严重问题。

✅ 正确做法:手动修复 constructor

js
编辑
Person.prototype = {
  constructor: Person, // 👈 显式指定,恢复正确指向
  sayHi() { console.log('Hi'); }
};

更严谨的方式(避免 constructor 被枚举):

js
编辑
Object.defineProperty(Person.prototype, 'constructor', {
  value: Person,
  writable: true,
  configurable: true,
  enumerable: false // 保持与原生一致
});

🔔 大厂面试高频考点:
“你在项目中是否遇到过 constructor 指向错误的问题?如何解决的?”


五、本质区分:Person ≠ Person.prototype

这是理解原型系统的分水岭:

表达式类型角色是否可调用
Person函数(Function)构造器(模具)✅ 可执行 new Person()
Person.prototype普通对象(Object)实例的公共模板❌ 不可调用

它们的关系是生产者与产品模板的关系:

  • Person 是制造汽车的工厂
  • Person.prototype 是所有汽车共用的方向盘、引擎设计图
js
编辑
console.log(Person === Person.prototype); // false
console.log(typeof Person);               // "function"
console.log(typeof Person.prototype);      // "object"

六、大厂面试模板:原型链核心问题精析

Q1:如何获取一个对象的原型?

A:推荐使用标准 API Object.getPrototypeOf(obj);也可用 obj.__proto__(非标准但兼容性好)。

Q2:a instanceof A 的原理是什么?

A:检查 A.prototype 是否出现在 a 的原型链上,即递归判断 a.__proto__ === A.prototype 或其上级。

Q3:为什么推荐将方法定义在原型上,而非构造函数内?

A:避免重复创建函数,节省内存。若在构造函数内定义方法,每个实例都会拥有独立的函数副本,无法共享。


结语

JavaScript 的原型系统摒弃了传统“类”的抽象,转而采用“对象链接”的哲学——每个对象都可以直接继承另一个对象的能力。这种设计虽初看晦涩,却赋予了语言无与伦比的灵活性与表达力。

掌握 “函数有 prototype,实例有 __proto__ 指向它” 这一核心,你就已超越多数开发者。下卷我们将深入 Object.create()、原型链查找算法、ES6 class 的语法糖本质,以及如何用原型思想设计高性能组件。敬请期待!

最后铭记
“原型不是继承,而是委托;不是复制,而是引用。理解链接,方得自由。”