JS中的原型及原型链
一、原型链
提起 JavaScript 的“原型对象”(Prototype),很多刚入门的小伙伴都会觉得脑子嗡的一下。别慌,我们继续用大白话和生活中的例子来攻克它!
如果说 Property(属性) 是贴在某个具体对象上的标签,那么 Prototype(原型对象) 就是这些对象的“共享模板”或“共享仓库”。
🏭 一个大白话比喻:共享充电宝 / 乐高积木
假设你和你的朋友们每人买了一台一模一样的“掌上游戏机”(对象)。
自有属性(Property): 你的游戏机贴了张贴纸,你朋友的游戏机外壳有划痕。这些属于你们各自机器的专属特征。
原型对象(Prototype): 厂家在出厂时,并没有给每台游戏机都内置1000款游戏(因为太占内存空间了)。厂家在云端放了一个“共享游戏库”,所有买这款游戏机的人,都可以通过联网直接玩里面的游戏。
这个“云端共享游戏库”,在 JavaScript 里就叫做 原型对象(Prototype)。
💻 为什么要用原型对象?(解决痛点)
我们来看一个实际的代码场景。假设我们要批量制造“小兵”对象:
1 | function Soldier(name) { |
问题来了: 如果我创建了 10 万个小兵,内存里就会有 10 万个一模一样的 attack 函数。这太浪费内存空间了!
💡 解决方案:把方法放进“原型对象”
我们把 attack 这个动作放进他们的“共享仓库”里,所有小兵共享这一个函数:
1 | function Soldier(name) { |
这时候,内存里只有 1 个 attack 函数。s1 和 s2 都可以跑去这个共享仓库里调用它。
🔍 JS 是怎么在原型里找东西的?(原型链)
当你在代码里写 s1.attack() 时,JavaScript 引擎会像侦探一样去破案,寻找这个属性:
- 第一步(看自己): 问 s1 自己:“你身上有 attack 这个属性吗?”
答:没有,我身上只有 name 和 hp。
- 第二步(看仓库): 顺着一根隐形的线,找到 s1 的原型对象(共享仓库),问:“你这里有 attack 吗?”
答:有的! 于是 JS 成功执行了该方法。
- 第三步(如果还没有): 如果原型对象里也没有,它会继续往更高级的“大原型”去找,直到找到头为止(最顶层是 Object.prototype)。如果一圈找下来都没有,就会报错 undefined。
这根由隐形线串起来的“寻找路线”,就叫做 原型链(Prototype Chain)。
🛠️ 现实中天天在用的例子
其实你天天都在用原型,只是你之前没发现。
还记得上一节的字符串吗?
1 | let str = "hello"; |
你并没有给 str 定义过 toUpperCase(转大写)这个方法对吧?那是谁给它的?
答案:它存在于字符串的“共享仓库”——String.prototype 里面。
📝 一分钟核心总结
什么是原型对象? 它是一个普通的对象,作为“共享仓库”存在,用来存放让同类对象共享的属性和方法。
为什么要用它? 节省内存。不需要给每个实例都复制一份一模一样的方法。
怎么运作的? 找属性时,自己有就用自己的;自己没有,就顺着“原型链”去共享仓库里找。
二、原型
在上面的例子中,Soldier 的原型对象,准确来说是 Soldier.prototype 指向的那个对象。
我们用一句话来定义它:
Soldier 的原型对象,就是 s1 和 s2(所有被制造出来的小兵)的“共享仓库”。
为了彻底理清,我们需要分清两个长得很像、但完全不同的“线索”。这也是所有 JS 初学者最容易绕晕的地方:
2.1. 它是构造函数的“属性”:Soldier.prototype
当你写下 function Soldier() {} 的那一刻,JavaScript 就会在幕后自动帮你做两件事:
1. 创建了这个名为 Soldier 的函数。
2. 自动给这个函数分配了一个附带的小本子(属性),叫做 prototype。这个小本子指向的位置,就是我们所说的原型对象(共享仓库)。
此时,这个仓库是空的。所以我们在代码里手动往里塞了一个攻击技能:
1 | Soldier.prototype.attack = function() { ... }; |
这时候,这个“原型对象”里就有一个叫做 attack 的方法了。
2.2 它是实例对象的“靠山”:proto
当你用 new Soldier(“小兵A”) 制造出 s1 时,JS 会在 s1 内部连接一根隐形的线,指向 Soldier 的那个共享仓库。
在代码里,我们可以通过 proto(前后各两个下划线,读作 “dunder proto”)来查看这根线连到了哪里:
1 | // 验证一下:s1 的“靠山/仓库” 是不是就是 Soldier 的那个小本子指向的地方? |
📊 一张图看清三者关系
我们可以把它们的关系画成一个铁三角:
1 | 【Soldier 构造函数】 |
对 Soldier 来说: “这是我出厂配置的共享仓库,写在我的 prototype 属性里。”
对 s1(小兵A)来说: “这是我的原型对象,我可以通过 proto 找到它。当我不会 attack 的时候,我就去这里偷学。”
💡 终极一问:那这个“原型对象”本身长什么样?
如果你在浏览器控制台打印出 Soldier.prototype,你会发现它本质上就是一个普通的 JavaScript 对象,里面长这样:
1 | { |
所以,Soldier 的原型对象,就是那个专门用来给所有小兵实例提供共享方法(如 attack)的、实实在在存在于内存中的对象。