JS原型链从原理到调试:__proto__和prototype别再混淆
如果你搜过“原型链 Prototype Chain”大概率是在准备面试或者被某段老代码里的__proto__绕进了坑。我第一次接触时也很抗拒总觉得这套东西像看不见摸不着的“亲戚关系”直接写 class 不香吗后来在项目里接了一段用原型链做插件注册的老代码花了大半个晚上排查一个属性到底是从哪来的才意识到原型链不是考试题是 JavaScript 底层真正跑着的复用逻辑。今天用一个尽量不绕的讲法把原型链是什么、__proto__和prototype到底有什么区别一次说清楚顺带聊聊很多人好奇的原型链补环境和 VSCode 调试技巧。1. 先想明白JS 为什么需要“链”1.1 没有 class 的日子代码靠什么复用JavaScript 诞生的时候是没有类的。ES6 虽然加了class但它本质还是语法糖底层依然走原型链。你可以把构造函数想象成“人”把它的prototype想象成一张标准图纸每次new出来的实例并不复制图纸内容而是持有一个指向图纸的线索。这样多个实例可以共享同一份方法不用每个人手里都揣一份拷贝。这个设计解决的核心问题是内存和一致性。如果每个对象都带着自己的方法浪费空间如果方法不一致后续维护就是噩梦。原型链提供了一种“共享但不复制”的机制方法在原型上写一份所有实例都借得到改的时候也只需要改一处。1.2 原型链是“借东西”的查找路线当一个对象访问某个属性时JavaScript 并不是只看对象本身而是做一次沿着原型链的逐层查找。查找顺序大致是先看对象自身有没有这个属性有就直接返回。如果没有就取对象内部隐藏的[[Prototype]]通常就是__proto__指向的那个对象继续找。再没有就顺着这个对象的[[Prototype]]一路往上。直到某个对象的原型是null查无可查返回undefined。这个过程中属性是“借”来的不是复制过来的。原型上的方法一旦变化所有依赖它的对象下一次访问都会拿到新结果。这也解释了为什么新同学经常遇到“改了原型方法老实例好像也变了”的诡异现象——因为在原型链上实例和实例之间本来就共享一份公共资源。1.3 一句话总结链的规则每个对象都有一个指向“出处”的隐藏指针规范里叫[[Prototype]]日常调试你大概率会看到__proto__。当你访问一个不存在的属性时JavaScript 会顺着这条指针一路问到“老祖宗”那里问到底都没有就返回undefined。函数身上那个显式存在的prototype则是决定“用new创建出来的实例它们的__proto__该指向谁”的样板对象。这两者一个在函数那边一个在实例这边职责完全不一样。注意class的extends本质上也是让子类的原型对象通过原型链指向父类的原型对象所以就算你平时只写 class这套查找规则依然在背后起作用。2. prototype 和proto职能分工完全不同2.1 prototype 是“函数”的私产普通函数能被new调用的函数身上自带一个prototype属性它指向一个普通对象。当你执行new Foo()时JavaScript 会把新对象的[[Prototype]]指向Foo.prototype。新实例本身没有prototype这个属于自己的属性只有函数才有。需要注意并不是所有函数都有prototype。箭头函数和对象里的方法简写不能被new调用所以它们没有prototype。这段代码验证一下function Foo() {} const obj {}; const Arrow () {}; console.log(typeof Foo.prototype); // object console.log(obj.prototype); // undefined console.log(Arrow.prototype); // undefined2.2proto是“对象”的出身证明几乎每个对象都有一个内置的[[Prototype]]指针指向上一个原型对象。为了方便开发者调试JavaScript 在Object.prototype上定义了__proto__访问器属性于是你可以在代码里直接读obj.__proto__甚至赋值。但真正规范推荐的读写方式是用Object.getPrototypeOf和Object.create而不是直接操作__proto__。可以这样理解__proto__像一扇窗户透过它能看到[[Prototype]]这个书架但窗户不等于书架本身。直接改__proto__有可能被自定义访问器劫持也可能带来性能问题。更稳妥的做法是const obj {}; console.log(Object.getPrototypeOf(obj) obj.__proto__); // true2.3 三者关系实例、构造函数、原型对象prototype、__proto__、constructor这三个东西经常一起出现用一张小表看最清楚概念谁拥有指向谁prototype函数该函数创建出的实例的__proto__指向它__proto__几乎每个对象该对象的“父级”对象constructorprototype对象拥有这个prototype的函数代码验证function Person(name) { this.name name; } const p new Person(小明); console.log(p.__proto__ Person.prototype); // true console.log(Person.prototype.constructor Person); // true console.log(p.constructor Person); // true最后一行特别容易误解p.constructor并不是p自己的属性而是它顺着原型链在Person.prototype上找到的。一旦有人把Person.prototype整个替换成新对象又没有手动补constructor那p.constructor就会跑到更上一层去导致类型判断出错。2.4 Function 和 Object 这两个“神仙站”容易绕晕JavaScript 里函数也是对象所以函数身上既有prototype也有__proto__于是出现了一串特别像是绕口令的关系Function.prototype本身是一个函数。Function.__proto__ Function.prototypeObject.__proto__ Function.prototypeFunction.prototype.__proto__ Object.prototypeObject.prototype.__proto__ null看起来很玄核心只需要记两条第一几乎所有的函数都由内置的Function构造出来所以它们的__proto__都指向Function.prototype第二普通对象往祖先方向一直走都会走到Object.prototype再往上是null。面试里问“为什么Function instanceof Object是 true”就是因为在Function的原型链上能找到Object.prototype。3. 实操手把手用 VSCode 把原型链“看”出来3.1 关于“vscode proto插件”的真相很多人搜“vscode proto插件”以为装一个叫 proto 的插件就能实时看原型链。坦白说我还没见过一个专门的插件叫这个名字。VSCode 自带的 JavaScript Debugger 在调试时已经会在变量面板里显示对象的[[Prototype]]不需要额外安装任何东西。如果非要追求“边写代码边看每行的执行结果”可以试试 Quokka.js不过免费版有限制。想快速观察原型链最实用的组合是打断点然后在调试控制台里执行Object.getPrototypeOf(obj)一层一层往下打。这个组合比任何插件都直接。3.2 一个最小 Demo亲手读出完整链条新建一个debug.js代码写简单点class Animal { constructor(name) { this.name name; } sayName() { console.log(this.name); } } class Dog extends Animal { bark() { console.log(汪汪); } } const dog new Dog(铁蛋); debugger;在 VSCode 里按 F5 启动调试左侧变量面板展开dog能一层层看到__proto__的嵌套。想一次看完整条链直接执行let p dog; while (p) { console.log(p.constructor.name); p Object.getPrototypeOf(p); } console.log(链条终点null);输出是Dog Animal Object 链条终点null你看dog本身只有namebark在Dog.prototype上sayName在Animal.prototype上再往上还能经过Object.prototype最后到null。这条逐层查找的路线就是最直观的原型链。3.3 动一发而牵全身修改原型后的连锁反应先看一个现象实例创建之后给构造函数原型加方法老实例立刻能访问到。function Dog(name) { this.name name; } Dog.prototype.bark function () { return 汪汪; }; const dog new Dog(铁蛋); console.log(dog.bark()); // 汪汪但如果你把Dog.prototype整个换掉情况就变了Dog.prototype { newMethod() { return 新方法; }, }; console.log(dog.bark()); // 还是汪汪因为旧实例的原型还指向旧对象 console.log(dog.newMethod); // undefined新对象不在它链上这个例子说明new出来的实例它的__proto__指向的是“当时那个原型对象”而不是“函数名字叫 Dog”。如果把原型整体替换新旧实例的原型就分道扬镳很多团队线上出怪问题根源常常就在这里。4. 进阶原型链补环境、继承实现与常见坑4.1 原型链补环境到底补的是什么我第一次看到“原型链补环境”这个词是在调试一段依赖浏览器环境的脚本时。某些代码会依赖navigator、location、document这些浏览器对象但你在 Node 环境跑直接报navigator is not defined。最简单的想法是声明一个全局变量顶上但很多代码会做更深入的检查比如遍历某个对象的keys、调用它原型上的方法甚至检查构造函数的链路这时候只给一个普通空对象就不够。补环境本质上就是把这些缺失对象按“它原本该有的样子”重建出来并且把原型链补齐。换句话说缺什么就补什么而不是把所有东西都绑到一个全局变量上。比如某个对象内部会调用Object.prototype.toString之类的通用方法那就确保对象来自一个正常原型链如果某个方法依赖构造出来的共享原型那就把对应函数的prototype也补齐。举个最简单的例子function patchEnv() { global.navigator { userAgent: Mozilla/5.0, language: zh-CN, }; } patchEnv(); console.log(navigator.userAgent);实际项目里补的东西更复杂但原则不变还原环境而不是污染公共原型。不要为了让某个对象“骗过”校验而去改Object.prototype那样做很容易连带污染整个运行环境。4.2 用原型链实现继承的三种姿势虽然现在可以直接写class extends但老代码里仍有大量基于原型的继承写法。最常见的组合式继承是这样function Parent(name) { this.name name; } Parent.prototype.getName function () { return this.name; }; function Child(name, age) { Parent.call(this, name); this.age age; } Child.prototype Object.create(Parent.prototype); Child.prototype.constructor Child; Child.prototype.getAge function () { return this.age; }; const child new Child(铁蛋, 3); console.log(child.getName()); // 铁蛋 console.log(child instanceof Child); // true console.log(child instanceof Parent); // true这里有三个关键点Parent.call(this, name)负责初始化父级里的实例字段否则拿不到name。Object.create(Parent.prototype)用来建立“子类原型 - 父类原型”的链路。如果图省事写Child.prototype Parent.prototype子类和父类共享同一个原型对象给子类扩展方法会直接污染父类。手动补Child.prototype.constructor Child是防止构造函数指向错乱。4.3 原生原型扩展的边界问题给String.prototype、Array.prototype、Object.prototype扩展方法在调试代码时确实方便比如String.prototype.firstChar function () { return this[0]; }; console.log(hello.firstChar()); // h但生产环境要非常克制。不同团队、不同依赖可能会定义同名方法一旦语义不一致就会出现“方法被悄悄覆盖”的问题。而且扩展可枚举属性会导致for...in把原型上的方法一并遍历出来。更稳妥的方案是封装独立函数或者用一个以实例为主键的WeakMap来保存扩展状态避免污染全局原型。4.4 别乱碰 Object.prototype两个奇怪 bug第一个是原型污染。如果某个库给Object.prototype加了一个可枚举的polluted属性那么你JSON.parse出的任意对象都会立刻读到它。维护者甚至可以构造一个包含__proto__字段的 JSON在一些解析逻辑不严谨的场景下影响原型链。规避办法是创建“干净对象”也就是Object.create(null)这种对象没有__proto__、没有constructor也自然不会被原型链上乱七八糟的属性干扰。第二个是for...in的“魔法遍历”。当Object.prototype上存在可枚举方法时for...in会把它当成所有对象的属性一起遍历出来。排查时你会看到一些莫名其妙的 key一脸懵。用hasOwnProperty可以过滤但最根本的还是别污染公共原型。5. 高频问题与排查技巧5.1 面试和实际开发都能用的速查表问题答案要点__proto__和prototype到底什么关系__proto__是对象指向父原型的链接prototype是函数给实例指定父原型的属性实例能直接访问构造函数的prototype属性吗不能只能通过实例.__proto__ 构造函数.prototype间接访问为什么[] instanceof Array是 true因为[]的原型链上能找到Array.prototypeObject.create(null)有什么特殊创建出来的对象没有原型没有__proto__也没有constructor适合做安全字典对象class 的原型和 function 的原型有区别吗机制一致但 class 的prototype属性不可写且 class 必须用new调用5.2 排查实录方法到底跑哪去了我遇到过一次很典型的问题。业务代码里有个utils对象一直在utils.format()后来新同事重构时重新导出了一个全新的utils但忘了带上原型上挂的format。结果运行时报utils.format is not a function。排查时先做的不是看代码逻辑而是用Object.prototype.hasOwnProperty.call(utils, format)判断方法是不是自身属性。结果发现不是自身属性那就说明问题不在“属性值”而在“原型链上根本没有这个方法”。继续用Object.getPrototypeOf(utils)一层层看最后确认新的utils被整体替换原来的原型被丢掉了。这个经验很实用报xxx is not a function不一定代表那个变量是undefined也可能只是它所在的原型链和你想的不一样。5.3 最后补两句调试心得在 VSCode 调试面板里看到对象时可以下意识展开它的[[Prototype]]多展开几次你对“哪些属性是自身、哪些是从原型借来的”会形成直觉。很多看似魔法一样的结果其实就是链上借来的属性。还有不要用obj.__proto__ someObj这种方式直接改原型。优先用Object.create或者Object.setPrototypeOf前者适合创建新对象时指定原型后者适合在已有对象上动态改链。直接碰__proto__访问器在极端情况下会触发自定义 getter/setter排查起来更难。把这些细节养成习惯原型链才真正算你手里的工具而不是背下来的考点。

相关新闻

前端开发中的null处理:从排查到治理的完整指南

前端开发中的null处理:从排查到治理的完整指南

1. 先定位:这个null到底是从哪一层冒出来的1.1 后端产出的null:数据库空值、ORM映射与聚合函数接手过中后台项目的人都懂一个规律:线上95%的"前端白屏事故",最后都能在一堆报错里翻到一个关键词——null。而最常见的场景…

2026/10/15 14:20:55 阅读更多 →
游戏辅助工具开发:从CGA框架到Lua脚本自动化的完整架构解析

游戏辅助工具开发:从CGA框架到Lua脚本自动化的完整架构解析

简介:这是基于CGA开源代码改进的魔力宝贝辅助工具MLAssist设计源码,面向具备C基础的游戏辅助开发者和魔力宝贝玩家,覆盖自动化操作、界面自定义、多语言脚本扩展及账号管理等功能场景。压缩包共2000个文件,以h/hpp头文件为主体&am…

2026/10/15 12:01:54 阅读更多 →
几百页投诉书堆在桌上,AI 怎么才能“读懂“一个案子?

几百页投诉书堆在桌上,AI 怎么才能“读懂“一个案子?

🌊 专注 AI 大模型与前沿科技深度解析,习惯从工程师视角拆解技术热点,让我们一起在技术浪潮中保持清醒与好奇 🚀几百页投诉书堆在桌上,AI 怎么才能"读懂"一个案子? 想象这样一个场景:…

2026/10/15 21:38:03 阅读更多 →

最新新闻

PyTorch深度学习实战:从环境搭建到模型训练与调试的完整指南

PyTorch深度学习实战:从环境搭建到模型训练与调试的完整指南

1. 从零上手PyTorch:为什么它成了深度学习项目的默认起点如果你最近打算认真做点深度学习相关的项目,不管是图像分类、文本生成还是推荐系统,大概率绕不开PyTorch这个名字。我身边不少做算法的朋友,从早期用别的框架折腾静态图&am…

2026/10/15 23:43:59 阅读更多 →
自用Agent全面功能测试:从工具调用到可靠性边界的实战梳理

自用Agent全面功能测试:从工具调用到可靠性边界的实战梳理

1. 为什么自用Agent必须做一次完整的“摸底测试”自用Agent这两年几乎是个人开发者、重度效率工具党的标配。我自己也是从最开始玩票性质地搭一个“能聊天的壳子”,到后来慢慢把日常的文本处理、信息汇总、定时任务都塞进去,它已经变成一个真正在干活的东…

2026/10/15 23:43:59 阅读更多 →
TensorFlow 2.x 实战入门:从环境搭建到模型部署的完整指南

TensorFlow 2.x 实战入门:从环境搭建到模型部署的完整指南

1. 从零上手 TensorFlow:先搞清楚它到底在解决什么问题很多人第一次接触 TensorFlow,是被“深度学习”“神经网络”这些词吓住的,觉得门槛高得离谱。但我带过不少刚入行的同学,真正卡住他们的往往不是数学,而是没弄明白…

2026/10/15 23:43:59 阅读更多 →
COMSOL模拟速凝浆液动水扩散规律与工程应用

COMSOL模拟速凝浆液动水扩散规律与工程应用

1. 项目背景与核心问题裂隙岩体注浆是地下工程防渗加固的关键技术,而速凝浆液在动水条件下的扩散行为直接决定了工程效果。传统经验公式法难以准确反映浆液与动水的相互作用机制,这促使我们采用COMSOL Multiphysics这一多物理场仿真利器,对两…

2026/10/15 23:43:59 阅读更多 →
390.安卓底层分区与启动链,EDL/BROM 救砖,自动化批量烧录完整源码

390.安卓底层分区与启动链,EDL/BROM 救砖,自动化批量烧录完整源码

摘要 本文面向具备一定计算机基础的开发者与维修人员,系统性地讲解安卓手机刷机与维修的底层逻辑。文章从安卓分区表与引导链原理出发,结合高通与联发科两大平台的硬件差异,提供一套完整的从解锁引导程序到线刷救砖的实操流程。同时,本文附带一个基于 Python 的自动化刷机脚…

2026/10/15 23:43:59 阅读更多 →
MXSPyCOM实战:Python COM封装驱动3ds Max的打包与调用全解析

MXSPyCOM实战:Python COM封装驱动3ds Max的打包与调用全解析

简介:MXSPyCOM源代码包专为3ds Max开发者、技术美术与流程工具使用者设计,通过将MaxScript和Python脚本统一交由外部编辑器编写、调试与执行,有效弥补内置编辑器功能单一的不足,适用于影视、游戏及可视化项目中需要批量处理、自定…

2026/10/15 23:42:58 阅读更多 →

日新闻

Docker Compose 容器化部署 ChirpStack LoRaWAN 网络服务器实战

Docker Compose 容器化部署 ChirpStack LoRaWAN 网络服务器实战

1. 为什么选择容器化方式部署 LoRa 网络服务LoRa 物联网项目落地时,网络服务器是整个链路的核心枢纽。它负责接收网关上报的数据包、完成去重与解密、再把有效载荷分发给上层应用。早期大家习惯把网络服务器直接装在物理机上,依赖一堆系统库和特定版本的…

2026/10/15 0:00:46 阅读更多 →
共享LLM服务下的跨模型自动扩缩容:Kubernetes实战与避坑指南

共享LLM服务下的跨模型自动扩缩容:Kubernetes实战与避坑指南

1. 共享推理服务里,扩缩容到底难在哪做过大模型推理服务部署的人,大概都经历过这样的场景:一个团队共用一套推理集群,A 业务跑的是 7B 的小模型,B 业务跑的是 72B 的大模型,C 业务白天流量高、晚上几乎归零…

2026/10/15 0:00:46 阅读更多 →
Systemd 启动阶段根文件系统只读之谜:fstab 与 remount-fs 服务深度解析

Systemd 启动阶段根文件系统只读之谜:fstab 与 remount-fs 服务深度解析

1. 引子:一次内核恐慌引发的追查先从一个真实场景说起。某次我给一台老旧的嵌入式设备折腾系统裁减,为了把启动时间压进两秒以内,我手贱地把一串自认为“没用”的 systemd 服务给 disable 了,其中就包括systemd-remount-fs.servic…

2026/10/15 0:00:46 阅读更多 →

周新闻

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

简介:基于 ARIMA、LSTM、Transformer 等模型的流感时间序列预测 Python 源码,面向计算机相关专业课程设计与期末大作业学生,以及项目实战学习者。内容覆盖预处理、平稳性检验、定阶、残差分析、多模型对比预测的完整时序建模流程,…

2026/10/15 16:18:54 阅读更多 →
影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别 做影刀RPA自动化,十个新手有八个栽在"往输入框里填东西"这件事上:要么填不进去,要么填了一半,要么直接把原来内容追加在后面。这背后的根因&…

2026/10/15 19:41:51 阅读更多 →
影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容 1. 认识影刀:什么场景该用RPA采小说数据 起点中文网的页面结构相对稳定——分类榜单、书籍详情、章节内容三块独立页面,跳转链路清晰。这种场景非常适合影刀自动化&#x…

2026/10/15 13:48:15 阅读更多 →

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/15 0:14:36 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/14 22:45:47 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/15 9:49:14 阅读更多 →