JS原型链从原理到调试:__proto__和prototype别再混淆

发布时间:2026/10/9 19:18:41

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排查起来更难。把这些细节养成习惯原型链才真正算你手里的工具而不是背下来的考点。
延伸阅读

更多相关文章

2026/10/9 19:18:41

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

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

2026/10/9 19:18:41

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

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

2026/10/9 19:18:41

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

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

2026/10/9 20:24:03

计算机组成原理入门:从冯·诺依曼结构到指令周期的核心概念

经常有刚学编程的同学问我:我为什么要知道CPU怎么取指令?我写Java不也能出活吗?每当被问到这个问题,我就知道对方还没建立起“计算机系统基础”的整体观。计算机的基本组成与基本概念,不是教科书第一章的过场戏&#x…

2026/10/9 20:24:03

基于Fluent UDF的电弧模拟:多物理场耦合实现与调试

电弧模拟这活儿,说白了就是在虚拟世界里徒手“造”一个电焊工:电弧区域的电流密度、温度、速度、磁场全都要同时算出来,而且这些场还是互相喂数据的。电流流过气体产生焦耳热,温度一上来电导率就变,电流分布跟着变&…

2026/10/9 20:24:03

智能体开发实战|基于Dify+MCP把天气信息推送到微信好友

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

2026/10/9 20:24:03

视频微表情识别中自适应关键帧算法:选帧、光流与序列模型实践

简介:面向计算机视觉与情感计算研究者的微表情识别项目,基于自适应关键帧思想处理视频中的瞬时面部变化,解决微表情持续时间短、特征微弱导致识别困难的问题。资源围绕视频预处理、关键帧检测、LBP/DoG特征提取、SVM/CNN分类及模型优化等环节…

2026/10/8 10:03:18

Jev+Agent接管浏览器:browser-use实战与jev-ultrafast性能优化

1. 从“Jev”说起:为什么我要把Agent接进浏览器“Jev”这个词最近在圈子里出现的频率越来越高,很多人第一次听到会以为是某个新模型的名字,其实它更像是一种思路——把Jev模型的能力当作底座,通过Agent的方式去接管浏览器&#xf…

2026/10/9 20:15:56

多智能体集群实战:DeepAgents编排、MCP与A2A协议及Skills体系

1. 从"单兵作战"到"集群协同":多智能体编排到底在解决什么问题如果你最近在折腾 Agent 相关的东西,大概率会有一种感觉:单个 Agent 能做的事情,其实很快就摸到天花板了。你给它一个提示词,挂几个工…

2026/10/8 6:05:44

无源低通滤波器设计实战:从RC到LC,手把手教你避开那些坑

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

2026/10/9 0:04:27

毕业论文初稿完成后首次进行AIGC疑似度自查的摸底与分流策略

毕业论文初稿完成后首次进行AIGC疑似度自查的摸底与分流策略当数万字的学位论文初稿经历开题、实验、问卷与多轮文献梳理最终成形时,绝大多数研究生都会面临一道全新的形式审查关卡:AIGC 疑似度排查。在高校毕业审核流程中,盲审前的文本检测通…

2026/10/9 0:04:27

食堂节能改造源头工厂,商用厨房设备焕新方案广受好评

商用厨房作为餐饮经营、单位供餐的核心后勤阵地,其设备配置、动线规划与运维体系直接决定后厨作业效率、运营成本与合规性。从基础的灶具、制冷存储设备,到油烟净化、水处理等配套系统,每一个环节的合理性都与食品安全、能耗管控、消防安全挂…

还想了解更多?直接咨询顾问

免费诊断 + 免费方案 + 透明报价。

全国咨询热线400-8866-253
免费获取方案
☎咨询二维码 ☎ ↑