🚀 从 "共享仓库" 到 "寻宝链":揭秘 JS 原型的隐藏法则(面试必看)
你是否也曾在调试代码时,对着控制台里的__proto__一脸茫然?是否疑惑过为什么两个实例能共享同一个方法,却又能拥有自己的独特属性?今天,我们就来扒开 JavaScript 中最 "低调却核心" 的机制 ——原型(prototype) 的神秘面纱。
🌰 一个让新手崩溃的场景
先看一段代码:
javascript 运行
// 定义一个"人"的构造函数
function Person(name) {
this.name = name;
}
// 给原型加一个"物种"属性
Person.prototype.species = "人类";
// 创建两个实例
const zhangsan = new Person("张三");
const lisi = new Person("李四");
console.log(zhangsan.species); // 输出"人类"
console.log(lisi.species); // 输出"人类"
console.log(zhangsan.species === lisi.species); // 输出true
明明没给张三和李四直接赋值species,它们却能 "共享" 这个属性?这就像两个陌生人共用一个储物柜 —— 而这个 "储物柜",就是原型。
📦 什么是原型?本质是 "共享仓库"
在 JavaScript 中,每个构造函数(首字母大写的函数,比如Person、Car)都自带一个prototype属性,它是一个对象。你可以把它理解为:给这个构造函数创建的所有实例准备的 "共享仓库" 。
- 存在于
prototype上的属性 / 方法,会被所有实例 "共享"(不用重复创建,节省内存) - 实例虽然没直接持有这些属性 / 方法,但能 "访问" 到它们(就像共享仓库的钥匙)
用代码举例:
javascript 运行
// 构造函数:汽车
function Car(color) {
this.color = color; // 每个实例独有的颜色(自己的东西)
}
// 原型上的共享方法(大家共用的工具)
Car.prototype.drive = function() {
console.log(`开着${this.color}的车下赛道~`);
};
const car1 = new Car("霞光紫");
const car2 = new Car("海湾蓝");
car1.drive(); // 开着霞光紫的车下赛道~
car2.drive(); // 开着海湾蓝的车下赛道~
这里的drive方法只在Car.prototype上存了一份,却能被car1和car2同时使用 —— 这就是原型的核心价值:减少重复,实现共享。
🔗 prototype vs __proto__:别再搞混了!
很多人刚学原型时,会被这两个长得像的东西绕晕。记住一句话:构造函数用prototype,实例用__proto__ 。
构造函数.prototype:共享仓库本身(存放共享内容)实例.__proto__:实例指向共享仓库的 "指针"(钥匙)
用代码验证:
javascript 运行
function Person(name) {
this.name = name;
}
Person.prototype.speci = "人类"; // 共享仓库
const su = new Person("舒老板");
// 实例的__proto__ 指向 构造函数的prototype
console.log(su.__proto__ === Person.prototype); // 输出true ✅
就像你(实例)用钥匙(__proto__)打开了仓库(prototype),才能拿到里面的东西(共享属性 / 方法)。
注意:
__proto__是浏览器提供的私有属性(非标准但常用),标准方法是用Object.getPrototypeOf(实例)获取原型。
🧬 原型链:对象的 "寻宝之路"
如果实例自己没有某个属性,它会顺着__proto__去原型里找;如果原型里也没有,会继续顺着原型的__proto__找 —— 这就是原型链,一条从实例通向 "根源" 的寻宝之路。
看一个完整的链条:
javascript 运行
const su = new Person("舒老板");
// 第一层:实例的原型是 Person.prototype
console.log(su.__proto__ === Person.prototype); // true
// 第二层:Person.prototype的原型是 Object.prototype(所有对象的默认原型)
console.log(su.__proto__.__proto__ === Object.prototype); // true
// 第三层:Object.prototype的原型是null(终点)
console.log(su.__proto__.__proto__.__proto__ === null); // true
当你调用su.toString()时,su自己没有toString,就会顺着链条找到Object.prototype.toString—— 这就是为什么任何对象都能调用toString!
💡 实战技巧:原型的正确用法
-
存放公共方法,节省内存避免在构造函数里定义方法(会重复创建),把方法放在
prototype上:javascript 运行
// 不好的写法 function Person(name) { this.name = name; this.sayHi = function() {}; // 每个实例都有一个sayHi,浪费内存 } // 推荐的写法 function Person(name) { this.name = name; } Person.prototype.sayHi = function() {}; // 所有实例共享一个sayHi -
实例属性会 "覆盖" 原型属性如果实例自己有某个属性,就不会去原型里找:
javascript
运行
Person.prototype.species = "人类"; const su = new Person("舒老板"); su.species = "LOL达人"; // 实例自己的属性 console.log(su.species); // 输出"LOL达人"(用自己的) console.log(su.__proto__.species); // 输出"人类"(原型上的还在) -
原型链继承(简单实现) 让一个构造函数的原型指向另一个构造函数的实例,实现继承:
javascript 运行
function Animal() {} Animal.prototype.species = "动物"; function Person() {} Person.prototype = new Animal(); // Person的原型指向Animal实例 const su = new Person(); console.log(su.species); // 输出"动物"(顺着链条找到Animal的原型)
🚨 避坑指南:这些错误不要犯!
-
直接修改
__proto____proto__是一个访问器属性,直接修改它的性能很差,并且会破坏代码的可预测性。如果你想改变一个对象的原型,应该使用Object.setPrototypeOf(obj, newProto)。javascript 运行
const obj = {}; const newProto = { x: 10 }; // ❌ 不推荐 obj.__proto__ = newProto; // ✅ 推荐 Object.setPrototypeOf(obj, newProto); -
混淆构造函数和普通函数只有通过
new关键字调用的函数才是构造函数,它的prototype才会发挥作用。直接调用构造函数会把this绑定到全局对象(非严格模式下),通常会导致意外的结果。javascript 运行
function Person(name) { this.name = name; } // ❌ 这不是创建实例!这会给全局对象添加name属性。 const wrongPerson = Person("错误的用法"); -
**忘记原型链的终点是
null**当在原型链上查找一个不存在的属性时,最终会查到Object.prototype.__proto__,而它的值是null。所以,访问一个不存在的属性会返回undefined,而不是报错。javascript 运行
const obj = {}; console.log(obj.nonExistentProperty); // 输出: undefined // console.log(obj.nonExistentProperty.method()); // 报错: Cannot read properties of undefined
📝 面试高频考点(划重点)
- 🔥
prototype和__proto__的区别?(上文已讲清) - 🔥 原型链的终点是什么?(
null) - 🔥 如何判断一个属性是实例自身的还是原型上的?(
obj.hasOwnProperty('属性名')) - 🔥 为什么说 JS 的面向对象是 "原型式" 而非 "类式"?(类式是血缘关系,原型式是共享机制)
总结:原型是 JS 的 "隐形骨架"
原型就像 JS 对象的 "共享基因",默默支撑着属性和方法的复用,也是理解继承、原型链的基础。看似抽象,实则藏在每一个对象的__proto__里。
下次再看到prototype,不妨把它想成一个贴满便利贴的共享冰箱 —— 每个实例都能打开,拿里面的东西,也能贴自己的便利贴(但不会覆盖冰箱原有的)。
你在学习原型时踩过哪些坑?欢迎在评论区分享你的经历~ 👇
(如果觉得有用,别忘了点赞 + 收藏,关注我解锁更多 JS 底层知识~)