前言
在前端开发的世界里,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 引擎会完成以下关键步骤:
- 创建一个全新的空对象:
{} - 将该对象的内部隐式属性
[[Prototype]]设置为Car.prototype - 将
Car函数中的this绑定到这个新对象,并执行函数体 - 如果函数没有显式返回对象,则默认返回这个新对象
其中第 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 的语法糖本质,以及如何用原型思想设计高性能组件。敬请期待!
最后铭记:
“原型不是继承,而是委托;不是复制,而是引用。理解链接,方得自由。”