news 2026/5/1 5:30:19

前端新人必懂:JavaScript原型链揭秘(从懵圈到上手实战)

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
前端新人必懂:JavaScript原型链揭秘(从懵圈到上手实战)


前端新人必懂:JavaScript原型链揭秘(从懵圈到上手实战)

  • 前端新人必懂:JavaScript原型链揭秘(从懵圈到上手实战)
    • 引言:你写的对象到底从哪继承了方法?
    • JavaScript原型链到底是个啥
    • 原型、构造函数和实例之间的三角关系
    • __proto__ 与 prototype 的恩怨情仇
    • Object.prototype 是不是所有对象的“老祖宗”
    • 为什么数组能用 push,对象却不能?——方法查找机制详解
    • 原型链的尽头在哪里?null 的终极地位
    • 修改原型的风险与正确姿势
      • 危险示范
      • 相对安全做法
    • 原型污染:那些年踩过的安全大坑
    • 性能考量:原型方法 vs 实例方法谁更快?
    • 调试原型链的实用技巧(console.log 看不懂?试试这些)
    • 手写一个简易继承体系,理解 ES6 class 背后的真相
    • 如何优雅地扩展内置对象而不惹祸上身
    • 常见误区大扫雷:比如“每个函数都有 prototype 吗?”
    • 开发中的真实场景:插件库、工具函数如何利用原型链
    • 当原型链遇上模块化:ESM 和 CommonJS 下的表现差异
    • 别再被面试官问倒:高频原型链面试题拆解思路
    • 让代码更聪明:基于原型链的动态行为注入技巧
    • 收个尾:原型链不是“八股文”,是“超能力”

前端新人必懂:JavaScript原型链揭秘(从懵圈到上手实战)

引言:你写的对象到底从哪继承了方法?

先别急着翻书,咱们玩个“找爸爸”的游戏。
打开浏览器控制台,敲两行代码:

constcat={name:'Miao'};cat.toString();// 竟然没报错?

你明明只给cat塞了一个name,它从哪偷来的toString
答案就藏在 JavaScript 的“原型链”里——一条看不见却随时跑出来背锅的继承链。
今天咱们不把概念念成经,而是把它拆成火锅料:一片肉、一颗丸子、一撮菜,涮明白为止。
准备好筷子,开锅!


JavaScript原型链到底是个啥

一句话:原型链就是“对象找不到属性时自动去‘爸爸’、‘爷爷’、‘祖宗’家里翻箱倒柜”的规则。
技术点说,每个对象都暗戳戳地挂着一个内部插槽[[Prototype]](浏览器给你露个接口叫__proto__),指向它的“原型对象”。
原型对象也是对象,也有自己的原型,于是链成一串,直到某位老祖宗null拍桌子喊“别找了,老子没有”。

画成地铁线路图:

cat({name:'Miao'}) ↓ __proto__ Object.prototype ↓ __proto__ null ← 终点站,下车

代码验证:

console.log(cat.__proto__===Object.prototype);// trueconsole.log(cat.__proto__.__proto__);// null

有图有真相,链就是这么直。


原型、构造函数和实例之间的三角关系

把“原型”想成共享仓库,把“构造函数”想成包工头,把“实例”想成精装房。
包工头手里握着一张图纸(prototype),每造一套房就把钥匙(__proto__)塞进房门,让房子共享仓库里的家具。

functionDog(name){this.name=name;// 每个实例自己的属性}Dog.prototype.bark=function(){console.log(`${this.name}: woof!`);// 共享方法};consthusky=newDog('Husky');constteddy=newDog('Teddy');husky.bark();// Husky: woof!teddy.bark();// Teddy: woof!// 共享同一份 barkconsole.log(husky.bark===teddy.bark);// true

三角恋关系图:

Dog.prototype ←→ 共享仓库 ↑ constructor | Dog (函数) | new husky / teddy (实例) ↓ __proto__ Dog.prototype

记住三句口诀:

  1. 函数的prototype指向仓库。
  2. 实例的__proto__指向仓库。
  3. 仓库的constructor指回函数。

proto与 prototype 的恩怨情仇

面试高频送命题:
__proto__prototype区别是啥?”
标准答案太枯燥,咱们讲段子。

prototype是“包工头的图纸”,只有函数才有;
__proto__是“房门的钥匙”,所有对象都有。
前者是“设计图”,后者是“导航仪”。
前者决定“共享什么”,后者决定“去哪找”。

代码版:

functionFoo(){}constf=newFoo();console.log(Foo.prototype);// 图纸在这儿console.log(f.__proto__);// 钥匙在这儿console.log(f.__proto__===Foo.prototype);// 图纸和钥匙对上了

但注意:__proto__已 deprecated,生产环境请用Object.getPrototypeOf/Reflect.getPrototypeOf,别写祖传代码被 linter 翻白眼。


Object.prototype 是不是所有对象的“老祖宗”

99% 的对象都认Object.prototype当干爹,但总有叛逆少年。
比如用Object.create(null)造出来的“字典对象”:

constmap=Object.create(null);map.foo=1;console.log(map.toString);// undefinedconsole.log(map.__proto__);// undefined

它天生没链子,所以写字典库时常用它,避免踩到hasOwnProperty这些地雷。
结论:Object.prototype不是所有人的爹,但它是“默认爹”。想绝交,就create(null)


为什么数组能用 push,对象却不能?——方法查找机制详解

push并不是数组对象的私货,而是挂在Array.prototype上的共享武器。
当你写[].push(1)时,引擎流程如下:

  1. 数组实例[]自己没有push
  2. 顺着__proto__找到Array.prototype,有了!
  3. 调用,完事。

对象{}的原型是Object.prototype,上面没有push,所以报错。
手动把数组的仓库借给对象用:

constobj={};Object.setPrototypeOf(obj,Array.prototype);obj.push(1,2,3);console.log(obj.length);// 3console.log(Array.isArray(obj));// false,外形是对象,灵魂是数组

这招叫“借链”,面试吹水可用,生产慎用,否则队友会打你。


原型链的尽头在哪里?null 的终极地位

引擎查找属性时,一路向上,直到:

obj.__proto__===null

才死心返回undefined
null就像“宇宙尽头的餐厅”,吃完就没下一站。
验证:

letp={};while(p){p=Object.getPrototypeOf(p);console.log(p);}// 输出:Object.prototype → null → 循环结束

修改原型的风险与正确姿势

危险示范

Array.prototype.push=function(){console.log('surprise!');};[].push();// 全体数组一起嗨,第三方库瞬间爆炸

相对安全做法

  1. 只扩展自家构造函数:
functionMyArray(){}MyArray.prototype=Object.create(Array.prototype);MyArray.prototype.push=function(...args){console.log('my push');returnArray.prototype.push.apply(this,args);};
  1. 使用Symbol避免命名冲突:
constcustomPush=Symbol('customPush');Array.prototype[customPush]=function(){console.log('safe push');};[].customPush();// 不会误伤他人
  1. 冻结原型:
Object.freeze(Array.prototype);// 谁也别想改

原型污染:那些年踩过的安全大坑

如果服务端把 JSON 直接merge到全局配置,攻击者注入__proto__字段就能污染所有对象:

constuserData=JSON.parse('{"__proto__": {"admin": true}}');constconfig={};Object.assign(config,userData);console.log({}.admin);// true,惊不惊喜?

防御手段:

  • JSON.parse后判断键名;
  • 使用Object.create(null)做 map;
  • 采用lodashmergeWith并过滤__proto__

性能考量:原型方法 vs 实例方法谁更快?

原型方法只存一份,节省内存;
实例方法每new一次就复制一份,内存爆炸,但少一次“链查找”,理论上更快。
实测说话:

functionProtoBench(){}ProtoBench.prototype.say=()=>{};functionInstanceBench(){this.say=()=>{};}constp=newArray(1e6).fill(null).map(_=>newProtoBench());consti=newArray(1e6).fill(null).map(_=>newInstanceBench());console.time('proto');p.forEach(x=>x.say());console.timeEnd('proto');console.time('instance');i.forEach(x=>x.say());console.timeEnd('instance');

Node 18 下结果(单位 ms):

proto: 28 instance: 45

原型方法不仅省内存,还更快——因为现代引擎对“链查找”做了内联优化。
结论:别用实例方法硬刚,除非你要做闭包缓存私有状态。


调试原型链的实用技巧(console.log 看不懂?试试这些)

  1. 树状打印:
functionprintChain(obj){constchain=[];letp=obj;while(p){chain.push(p);p=Object.getPrototypeOf(p);}console.table(chain.map((x,i)=>({level:i,ctor:x.constructor.name,keys:Object.getOwnPropertyNames(x).join(', ')})));}printChain([]);// 一目了然
  1. 断点调试:
    DevTools → Scope →[[Prototype]]直接展开,比console.log翻山越岭香。

  2. console.dir
    console.dir(husky)勾选 “Show prototypes”,链上每一环都裸奔。


手写一个简易继承体系,理解 ES6 class 背后的真相

ES6class只是语法糖,骨子里还是原型链。
咱们手写一套“继承”复刻:

// 父类functionAnimal(name){this.name=name;}Animal.prototype.speak=function(){return`${this.name}makes a noise.`;};// 子类functionDog(name,breed){Animal.call(this,name);// 借父构造函数初始化属性this.breed=breed;}// 核心:原型链继承Dog.prototype=Object.create(Animal.prototype);Dog.prototype.constructor=Dog;// 修正指针Dog.prototype.speak=function(){return`${this.name}barks.`;// 多态};constd=newDog('Milo','Labrador');console.log(d.speak());// Milo barks.console.log(dinstanceofDog);// trueconsole.log(dinstanceofAnimal);// true

对比 ES6:

classAnimal{constructor(name){this.name=name;}speak(){return`${this.name}makes a noise.`;}}classDogextendsAnimal{constructor(name,breed){super(name);this.breed=breed;}speak(){return`${this.name}barks.`;}}

Babel 转译后就是上面的function版,毫无魔法。


如何优雅地扩展内置对象而不惹祸上身

需求:给数组加一个“安全求和”方法,但别污染全局原型。
解:用“子类化”+Symbol,组合拳出击:

constsumSym=Symbol('sum');classSafeArrayextendsArray{[sumSym](){returnthis.reduce((a,b)=>a+b,0);}// 提供公开接口sum(){returnthis[sumSym]();}}constarr=SafeArray.from([1,2,3]);console.log(arr.sum());// 6console.log([][sumSym]);// undefined,原生数组不受影响

核心思路:

  1. 继承内置构造函数;
  2. Symbol做键名,防碰撞;
  3. 公开方法做代理,保持封装。

常见误区大扫雷:比如“每个函数都有 prototype 吗?”

  • 误区 1:箭头函数也有prototype
    真相:箭头函数没有prototype属性,也不能new
constFoo=()=>{};console.log(Foo.prototype);// undefined
  • 误区 2__proto__是 ECMA 标准属性
    真相:只是浏览器事实标准,ES6 起被Object.getPrototypeOf取代。

  • 误区 3:修改__proto__会立刻提升性能
    真相:频繁改动原型会 de-opt,引擎会放弃隐藏类优化,性能反而雪崩。


开发中的真实场景:插件库、工具函数如何利用原型链

案例:写一款“链式”字符串工具库,模仿lodash/fp

functionMyStr(val){if(!(thisinstanceofMyStr))returnnewMyStr(val);this.val=val;}MyStr.prototype.map=function(fn){returnnewMyStr(fn(this.val));};MyStr.prototype.toUpper=function(){returnthis.map(s=>s.toUpperCase());};MyStr.prototype.value=function(){returnthis.val;};console.log(MyStr('kimi').toUpper().value());// KIMI

利用原型共享方法,避免每次链式都复制函数,内存占用极低。
再进一步,把MyStr.prototype做成冻结单例,防止外部篡改:

Object.freeze(MyStr.prototype);

当原型链遇上模块化:ESM 和 CommonJS 下的表现差异

在 CommonJS 里,每个文件是一个“闭包对象”,如果给Array.prototype加料,会影响整个运行时;
在 ESM 里,行为一致,但静态导入让“污染”更容易被 tree-shaking 误杀——
比如你把扩展方法挂在原型上,却没在模块里显式引用,打包器可能把“看似无用”的扩展剔除。
解决:

  1. 扩展原型时,在模块里执行一次“自调用”代码,确保副作用保留;
  2. 或者写sideEffects: true告诉打包器“别删我”。

别再被面试官问倒:高频原型链面试题拆解思路

题 1:如何让a === 1 && a === 2 && a === 3成立?
思路:利用valueOf在原型链上的动态查找:

constobj=Object.create(null);letval=1;Object.defineProperty(obj,'valueOf',{value(){returnval++;}});consta=obj;console.log(a==1&&a==2&&a==3);// true

题 2:说出new的四个步骤

  1. 创建空对象;
  2. 链接到构造函数原型;
  3. 绑定this执行构造函数;
  4. 返回对象(或构造函数的显式返回)。

题 3:如何判断一个对象是否是“普通对象”?

functionisPlainObject(o){returnObject.prototype.toString.call(o)==='[object Object]'&&(Object.getPrototypeOf(o)===null||Object.getPrototypeOf(o)===Object.prototype);}

让代码更聪明:基于原型链的动态行为注入技巧

场景:运行时根据用户权限给按钮注入“点击上报”逻辑,但不想改原组件。
解:把方法注入到原型链的“代理层”,实现无侵入 AOP。

// 原始按钮类functionButton(label){this.label=label;}Button.prototype.click=function(){console.log(`Button${this.label}clicked`);};// 权限装饰器functionwithReport(Base){functionProxyButton(...args){Base.apply(this,args);}ProxyButton.prototype=Object.create(Base.prototype);ProxyButton.prototype.click=function(){console.log('[Report] send log');Base.prototype.click.call(this);};returnProxyButton;}constReportButton=withReport(Button);constbtn=newReportButton('Save');btn.click();// [Report] send log// Button Save clicked

利用原型链的“层级差”,实现逻辑插拔,比直接改源码清爽一百倍。


收个尾:原型链不是“八股文”,是“超能力”

很多人学完原型链,只在面试时背概念,写完 class 就再也不回头。
其实它像浏览器留给你的一把瑞士军刀:

  • 能做性能优化(共享方法);
  • 能做安全隔离(create(null));
  • 能做动态代理(行为注入);
  • 甚至能做“元编程”——在运行时重写对象的语言层逻辑。

别害怕它的“古老”,也别被class糖衣迷惑。
真正的“上手”,是多写一行代码、多踩一次坑、多看一次__proto__的指向,然后拍拍键盘:“哦,原来如此。”
到那时,你写的就不再是代码,而是 JavaScript 的“家谱”。

欢迎来到我的博客,很高兴能够在这里和您见面!希望您在这里可以感受到一份轻松愉快的氛围,不仅可以获得有趣的内容和知识,也可以畅所欲言、分享您的想法和见解。

推荐:DTcode7的博客首页。
一个做过前端开发的产品经理,经历过睿智产品的折磨导致脱发之后,励志要翻身农奴把歌唱,一边打入敌人内部一边持续提升自己,为我们广大开发同胞谋福祉,坚决抵制睿智产品折磨我们码农兄弟!


专栏系列(点击解锁)学习路线(点击解锁)知识定位
《微信小程序相关博客》持续更新中~结合微信官方原生框架、uniapp等小程序框架,记录请求、封装、tabbar、UI组件的学习记录和使用技巧等
《AIGC相关博客》持续更新中~AIGC、AI生产力工具的介绍,例如stable diffusion这种的AI绘画工具安装、使用、技巧等总结
《HTML网站开发相关》《前端基础入门三大核心之html相关博客》前端基础入门三大核心之html板块的内容,入坑前端或者辅助学习的必看知识
《前端基础入门三大核心之JS相关博客》前端JS是JavaScript语言在网页开发中的应用,负责实现交互效果和动态内容。它与HTML和CSS并称前端三剑客,共同构建用户界面。
通过操作DOM元素、响应事件、发起网络请求等,JS使页面能够响应用户行为,实现数据动态展示和页面流畅跳转,是现代Web开发的核心
《前端基础入门三大核心之CSS相关博客》介绍前端开发中遇到的CSS疑问和各种奇妙的CSS语法,同时收集精美的CSS效果代码,用来丰富你的web网页
《canvas绘图相关博客》Canvas是HTML5中用于绘制图形的元素,通过JavaScript及其提供的绘图API,开发者可以在网页上绘制出各种复杂的图形、动画和图像效果。Canvas提供了高度的灵活性和控制力,使得前端绘图技术更加丰富和多样化
《Vue实战相关博客》持续更新中~详细总结了常用UI库elementUI的使用技巧以及Vue的学习之旅
《python相关博客》持续更新中~Python,简洁易学的编程语言,强大到足以应对各种应用场景,是编程新手的理想选择,也是专业人士的得力工具
《sql数据库相关博客》持续更新中~SQL数据库:高效管理数据的利器,学会SQL,轻松驾驭结构化数据,解锁数据分析与挖掘的无限可能
《算法系列相关博客》持续更新中~算法与数据结构学习总结,通过JS来编写处理复杂有趣的算法问题,提升你的技术思维
《IT信息技术相关博客》持续更新中~作为信息化人员所需要掌握的底层技术,涉及软件开发、网络建设、系统维护等领域的知识
《信息化人员基础技能知识相关博客》无论你是开发、产品、实施、经理,只要是从事信息化相关行业的人员,都应该掌握这些信息化的基础知识,可以不精通但是一定要了解,避免日常工作中贻笑大方
《信息化技能面试宝典相关博客》涉及信息化相关工作基础知识和面试技巧,提升自我能力与面试通过率,扩展知识面
《前端开发习惯与小技巧相关博客》持续更新中~罗列常用的开发工具使用技巧,如 Vscode快捷键操作、Git、CMD、游览器控制台等
《photoshop相关博客》持续更新中~基础的PS学习记录,含括PPI与DPI、物理像素dp、逻辑像素dip、矢量图和位图以及帧动画等的学习总结
日常开发&办公&生产【实用工具】分享相关博客》持续更新中~分享介绍各种开发中、工作中、个人生产以及学习上的工具,丰富阅历,给大家提供处理事情的更多角度,学习了解更多的便利工具,如Fiddler抓包、办公快捷键、虚拟机VMware等工具

吾辈才疏学浅,摹写之作,恐有瑕疵。望诸君海涵赐教。望轻喷,嘤嘤嘤

非常期待和您一起在这个小小的网络世界里共同探索、学习和成长。愿斯文对汝有所裨益,纵其简陋未及渊博,亦足以略尽绵薄之力。倘若尚存阙漏,敬请不吝斧正,俾便精进!

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/4/23 20:37:19

AI Agent自治系统离我们还有多远?

AI Agent自治系统离我们还有多远? 在今天,当你对着语音助手说“帮我订一张明天去上海的高铁票”,它不仅能听懂你的指令,还能自动打开购票App、查询车次、填写信息,甚至提醒你带身份证——这已经不再是科幻电影的情节。…

作者头像 李华
网站建设 2026/4/24 21:24:47

NeverSink过滤器:PoE2玩家的智能寻宝伙伴

你的游戏困扰诊断报告 【免费下载链接】NeverSink-Filter-for-PoE2 This is a lootfilter for the game "Path of Exile 2". It adds colors, sounds, map icons, beams to highlight remarkable gear and inform the user 项目地址: https://gitcode.com/gh_mirro…

作者头像 李华
网站建设 2026/4/27 20:35:42

DiffPDF V6.0.0实战指南:精准识别PDF文档差异的高效解决方案

DiffPDF V6.0.0实战指南:精准识别PDF文档差异的高效解决方案 【免费下载链接】DiffPDFV6.0.0强大的PDF文件比较工具 DiffPDF V6.0.0 是一款功能强大的PDF文件比较工具,专为高效识别和展示PDF文件间的文本与布局差异而设计。无论是软件开发中的版本更新&a…

作者头像 李华
网站建设 2026/4/24 9:18:03

Vue-ECharts + ECharts GL:打造沉浸式3D数据可视化体验的完整指南

Vue-ECharts ECharts GL:打造沉浸式3D数据可视化体验的完整指南 【免费下载链接】vue-echarts 项目地址: https://gitcode.com/gh_mirrors/vue/vue-echarts 还在为平面图表无法生动展示复杂数据而烦恼吗?🤔 想要让数据"站起来&…

作者头像 李华
网站建设 2026/4/25 19:34:14

Inspector Spacetime 终极指南:动画提取与设计开发协作工具

Inspector Spacetime 终极指南:动画提取与设计开发协作工具 【免费下载链接】inspectorspacetime Inject motion specs into reference video to become an engineers best friend 项目地址: https://gitcode.com/gh_mirrors/in/inspectorspacetime Inspecto…

作者头像 李华