当我把 proto 打印出来那一刻,我懂了JS的原型链
💬 前言:我本以为我会面向对象,结果我连“对象”都没搞懂
刚开始学 JavaScript 的时候,我以为:
function Person(name) {
this.name = name;
}
const p1 = new Person('小明');
console.log(p1.name); // 小明
这不就是面向对象吗?简单!
直到有一天,我在控制台敲下:
console.log(p1.__proto__);
然后——我的世界崩塌了。
满屏的 [[Prototype]]、constructor、__proto__……我仿佛掉进了一个无限嵌套的俄罗斯套娃里。
“我是谁?”
“我从哪里来?”
“我要到哪里去?”
——来自一个被原型链逼疯的学生的灵魂三问。
今天,就用一个真实学习者的视角,带你从困惑到理解,一步步揭开 prototype 的神秘面纱。没有高深术语,只有大白话 + 可运行代码 + 我踩过的坑。
🚪 一、为什么需要原型?—— 因为我不想每个对象都背一份方法
假设我们要创建多个学生对象:
❌ 错误写法:每个学生都自带“技能包”
function Student(name) {
this.name = name;
// 每个学生都独立拥有一个 sayHello 方法
this.sayHello = function() {
console.log(`大家好,我是${this.name}`);
};
}
const s1 = new Student('张三');
const s2 = new Student('李四');
console.log(s1.sayHello === s2.sayHello); // false → 完全不同的两个函数!
问题来了:如果创建 1000 个学生,就会有 1000 个 sayHello 函数,内存直接爆炸 💥。
这就像学校给每个学生发一本《礼仪手册》,其实大家看的都是同一本书,但每人一本——太浪费了!
✅ 正确姿势:把公共方法放进“共享书架”(prototype)
function Student(name) {
this.name = name; // 每个学生独有的属性
}
// 所有学生共享的方法,统一挂载到 prototype 上
Student.prototype.sayHello = function() {
console.log(`大家好,我是${this.name}`);
};
const s1 = new Student('张三');
const s2 = new Student('李四');
console.log(s1.sayHello === s2.sayHello); // true → 同一个函数,只存一份!
s1.sayHello(); // 大家好,我是张三
s2.sayHello(); // 大大家好,我是李四
📌 我的理解:
-
prototype就是构造函数的“共享书架” - 实例自己没有的方法,会自动去书架上找
- 既节省内存,又方便统一管理
这就是原型存在的意义:让对象学会“蹭”!
🔗 二、四大核心概念:别再混淆 prototype 和 proto 了!
刚开始我总分不清 prototype 和 __proto__,后来我画了张图,终于懂了。
1️⃣ 构造函数:创建实例的“模板”
function Student(name) {
this.name = name;
}
它就是一个普通函数,但通常:
- 首字母大写
- 用
new调用
new 的过程可以简化为:
- 创建空对象
{}; - 把
this指向它; - 执行函数体;
- 返回这个对象。
2️⃣ prototype:构造函数的“共享仓库”
每个函数都有一个 prototype 属性,它是一个对象,用来存放所有实例共享的内容。
Student.prototype.species = '人类';
Student.prototype.study = function() {
console.log(`${this.name}在努力学习`);
};
⚠️ 注意:prototype 是函数才有的属性!
3️⃣ __proto__:实例通往原型的“梯子”
每个对象(包括实例)都有一个 __proto__ 属性(非标准但广泛支持),它指向其构造函数的 prototype。
const s1 = new Student('张三');
console.log(s1.__proto__ === Student.prototype); // true
👉 这就是实例能访问到 sayHello 的原因:s1.sayHello() → 自己没有 → 顺着 __proto__ 找 → 找到 Student.prototype.sayHello
🎯 记住一句话:实例的
__proto__指向构造函数的prototype
4️⃣ constructor:原型的“回老家按钮”
原型对象上有一个 constructor 属性,指向构造函数本身。
console.log(Student.prototype.constructor === Student); // true
console.log(s1.constructor === Student); // true
⚠️ 重要提醒:手动重写 prototype 要修复 constructor!
Student.prototype = {
sayHello() { console.log('hi') }
};
const s1 = new Student('张三');
console.log(s1.constructor === Student); // false ❌
console.log(s1.constructor === Object); // true → 错了!
// ✅ 修复:
Student.prototype = {
constructor: Student,
sayHello() { console.log('hi') }
};
否则后续 instanceof 判断可能出错。
📊 核心关系图(建议收藏)
![]()
📌 再说一遍:实例的 __proto__ 指向构造函数的 prototype,原型的 constructor 指向构造函数。
🔍 三、原型查找机制:JS是怎么找到方法的?
当你调用 s1.sayHello() 时,JS 引擎是这样找的:
- 先看
s1自己有没有sayHello; - 没有?那就通过
__proto__去Student.prototype找; - 还没有?继续通过
Student.prototype.__proto__找上一级; - 直到找到,或者查到
null。
这个链条,就是原型链。
🖼️JavaScript 原型链完整关系图
![]()
1. 查找示例
function Student(name) {
this.name = name;
}
Student.prototype.species = '人类';
Student.prototype.study = function() {
console.log(`${this.name}在学习`);
};
const s1 = new Student('张三');
console.log(s1.name); // 张三 → 自身属性
console.log(s1.species); // 人类 → 来自 prototype
console.log(s1.toString()); // [object Object] → 来自 Object.prototype
console.log(s1.abc); // undefined → 找不到
2. 原型链终点:null
console.log(Object.prototype.__proto__); // null → 终点!
// 验证整个链:
console.log(s1.__proto__); // Student.prototype
console.log(s1.__proto__.__proto__); // Object.prototype
console.log(s1.__proto__.__proto__.__proto__); // null
3. 实例属性可以“屏蔽”原型属性
function Student(name) {
this.name = name;
this.species = '外星人'; // 覆盖原型属性
}
Student.prototype.species = '人类';
const s1 = new Student('张三');
console.log(s1.species); // 外星人
delete s1.species;
console.log(s1.species); // 人类 → 删除后重新查找原型
✅ 应用:为个别实例定制行为,不影响全局。
🧬 四、原型式继承:JS的“继承”到底是什么?
传统语言是“类继承”(血缘关系),而 JS 是“委托继承”——你不会,就去问你爸,你爸不会,就去问爷爷。
1. 经典继承实现
// 父类
function Person(name) {
this.name = name;
}
Person.prototype.greet = function() {
console.log(`你好,我是${this.name}`);
};
// 子类
function Student(name, grade) {
Person.call(this, name); // 继承父类实例属性
this.grade = grade;
}
// 继承父类原型方法
Student.prototype = new Person();
Student.prototype.constructor = Student;
// 扩展子类方法
Student.prototype.study = function() {
console.log(`${this.name}在读${this.grade}年级`);
};
const s1 = new Student('张三', 3);
s1.greet(); // 你好,我是张三(继承)
s1.study(); // 张三在读3年级(自有)
2. ES6 class 只是语法糖
class Person {
constructor(name) { this.name = name; }
greet() { console.log(`你好,我是${this.name}`); }
}
class Student extends Person {
constructor(name, grade) {
super(name);
this.grade = grade;
}
study() { console.log(`${this.name}在读${this.grade}年级`); }
}
底层依然是原型链驱动。class 不是新东西,只是让你写得更爽。
💡 五、原型的实际应用
1. 工具类共享方法
function Utils() {}
Utils.prototype.formatDate = function(date) { /* ... */ };
2. 扩展原生对象(谨慎!)
Array.prototype.unique = function() {
return [...new Set(this)];
};
[1,2,2,3].unique(); // [1,2,3]
⚠️ 注意:生产环境慎用,避免污染全局。
3. 单例模式
function Singleton() {
if (Singleton.prototype.instance) {
return Singleton.prototype.instance;
}
this.data = '唯一实例';
Singleton.prototype.instance = this;
}
4. 框架中的应用(如 Vue)
Vue.prototype.$http = axios; // 所有组件都能用 this.$http
⚠️ 六、常见误区
❌ 误区1:混淆 prototype 和 proto
-
prototype:函数才有,是“仓库” -
__proto__:对象都有,是“梯子”
❌ 误区2:覆盖 prototype 不修 constructor
会导致 instanceof 失效。
✅ 正确做法:永远记得修 constructor!
🏁 七、总结:原型是JS的灵魂
| 核心要点 | 说明 |
|---|---|
| 🔹 核心价值 | 共享方法,节省内存 |
| 🔹 核心关系 | 实例.__proto__ === 构造函数.prototype |
| 🔹 查找机制 | 自身 → 原型链 → null
|
| 🔹 继承本质 | 委托查找,非类继承 |
| 🔹 class 本质 | 原型的语法糖 |
🌟 最后感悟:
学原型的过程,就像在迷宫中找出口。
一开始觉得混乱,但当你画出那张关系图,执行第一段可运行代码,听到“啊哈!”的那一声——
你就真正理解了 JavaScript 的灵魂。