ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Modern JavaScript 教程精读:修改构造函数的 `prototype` 之后,已有实例会发生什么变化?

Modern JavaScript 教程精读:修改构造函数的 `prototype` 之后,已有实例会发生什么变化? 文档/教程前端【免费下载链接】en.javascript.infoModern JavaScript Tutorial项目地址https://gitcode.com/gh_mirrors/en/en.javascript.info点击查看免费下载本篇文章以《Modern JavaScript Tutorial》en.javascript.info中「Changing prototype」习题为核心完整还原四种修改Rabbit.prototype的代码场景逐一推导alert(rabbit.eats)的输出结果并从F.prototype与[[Prototype]]的底层机制出发解释每一条结论的原理。读完本文你将彻底理解「重新赋值 prototype」与「修改 prototype 的属性」对既有实例和未来实例的不同影响以及delete操作在原型链上的真实行为能够在实际项目中规避一类高频原型链陷阱。一、习题背景先厘清F.prototype的作用时机要理解本习题的四个场景必须先回顾该习题所属章节 F.prototype 的核心结论F.prototype是构造函数F上的一个普通属性与内部隐藏属性[[Prototype]]完全不同它只在new F()被调用时起作用为新创建的对象设置[[Prototype]]。关键推论F.prototype只在new F的时刻生效。如果创建对象之后F.prototype被整体替换F.prototype 另一个对象新对象会以新对象为原型而已有对象保留创建时的那一个原型。对象的属性读取会沿[[Prototype]]链向上查找而写入/删除操作则直接作用于对象自身不经过原型链数据属性场景下。下图展示了Rabbit.prototype animal之后rabbit与animal之间的关系水平箭头表示普通属性prototype垂直箭头表示真正的继承关系[[Prototype]]习题中的所有代码都建立在这两条规则之上对象创建的那一刻原型就被固定了此后对属性的一切修改含delete都只作用于当前对象自身。二、题目全貌四段代码、四次渐进改动习题原文见 task.md初始代码如下function Rabbit() {} Rabbit.prototype { eats: true }; let rabbit new Rabbit(); alert( rabbit.eats ); // true对象rabbit由new Rabbit()创建其[[Prototype]]被设置为Rabbit.prototype即{ eats: true }。由于rabbit自身没有eats属性读取rabbit.eats时沿原型链找到原型上的eats: true因此输出true。接下来的四个问题分别通过一条语句的改动来考察你对原型机制的掌握场景 1整体重新赋值Rabbit.prototypefunction Rabbit() {} Rabbit.prototype { eats: true }; let rabbit new Rabbit(); Rabbit.prototype {}; alert( rabbit.eats ); // ?场景 2修改原型对象的属性function Rabbit() {} Rabbit.prototype { eats: true }; let rabbit new Rabbit(); Rabbit.prototype.eats false; alert( rabbit.eats ); // ?场景 3删除实例自身的属性function Rabbit() {} Rabbit.prototype { eats: true }; let rabbit new Rabbit(); delete rabbit.eats; alert( rabbit.eats ); // ?场景 4删除原型上的属性function Rabbit() {} Rabbit.prototype { eats: true }; let rabbit new Rabbit(); delete Rabbit.prototype.eats; alert( rabbit.eats ); // ?三、逐场景解析四种改动四种结局官方解答见 solution.md四个答案依次是true、false、true、undefined。下面逐一推导。场景 1Rabbit.prototype {}→true对Rabbit.prototype的赋值会为新对象设置[[Prototype]]但不会影响已存在的对象。new Rabbit()发生在Rabbit.prototype {}之前因此rabbit的[[Prototype]]早就指向了最初的{ eats: true }。此后Rabbit.prototype {}只是改变了构造函数上的普通属性让未来的new Rabbit()以新对象为原型rabbit的原型链丝毫未动依旧能找到eats: true。这正是章节中强调的F.prototype只在new F时被读取一次。已创建的对象与后续的原型改动完全绝缘。场景 2Rabbit.prototype.eats false→false对象是按引用赋值的。Rabbit.prototype指向的对象不会被复制rabbit的[[Prototype]]与Rabbit.prototype引用的是同一个对象。因此通过其中一个引用修改内容另一个引用也能看到。场景 2 没有替换Rabbit.prototype这个属性本身而是修改了它所指向对象的内部状态。rabbit的原型链指向的正是这个对象所以rabbit.eats的读取沿链找到了被改写后的false。对比场景 1 与场景 2可以提炼出一条判断口诀Rabbit.prototype ...赋值给属性只改变构造函数属性影响未来实例不影响既有实例Rabbit.prototype.xxx ...修改属性指向的对象改变的是原型对象本身既有实例通过[[Prototype]]引用同一对象立即可见。场景 3delete rabbit.eats→true所有delete操作都直接作用于对象自身。delete rabbit.eats尝试从rabbit上删除eats但rabbit自己并没有这个属性它在原型上因此删除操作不产生任何效果。这印证了原型链的核心读取/写入规则详见 01-prototype-inheritance/article.md 中的「Writing doesnt use prototype」小节读取属性才会沿原型链回溯写入与删除则永远只操作对象自身。rabbit上没有eats可删delete静默返回true并继续让原型链上的eats: true生效所以alert输出true。场景 4delete Rabbit.prototype.eats→undefinedeats属性从原型上被真正删除原型链上再也找不到它于是rabbit.eats的读取以undefined告终。与场景 3 不同这次delete的目标是原型对象本身。delete Rabbit.prototype.eats成功删除了{ eats: true }中的eats属性rabbit自身又没有该属性沿着[[Prototype]]链向上也找不到任何eats最终整个查找以失败告终读取表达式返回undefined。四、原理纵深四组结论背后的语言机制4.1 对象按引用共享替换与修改是两回事场景 1 与场景 2 的分野本质是引用赋值替换指向与通过引用修改对象内容的区别。rabbit的[[Prototype]]保存的是对原型对象的引用而非原型的快照。只要原型对象还活着rabbit就能看到它的一切变化但把Rabbit.prototype指向一个全新对象并不会改变rabbit手中已有的引用。4.2 读取走原型链写入与删除不走场景 3 与场景 4 展示了delete的「对象自身优先」语义。可以借助 01-prototype-inheritance/article.md 中的原型链图景来记忆读rabbit.eatsrabbit→ 原型{eats: true}→Object.prototype→null逐级向上找写 /delete rabbit.eats只作用于rabbit自身绝不触碰原型delete Rabbit.prototype.eats作用于原型对象自身删掉后链上彻底断供。4.3 默认prototype与constructor的连带影响为什么章节反复提醒「不要整体覆盖F.prototype」因为每个函数默认带有F.prototype { constructor: F }见 function-prototype-constructor.svg一旦像场景 1 那样把Rabbit.prototype整体替换为{}constructor属性也随之丢失rabbit.constructor Rabbit将变为false。如果确实需要整体替换应当手动补回constructorRabbit.prototype { eats: true, constructor: Rabbit // 手动补回保持正确 };或者只增改默认prototype的属性保留原有的constructorRabbit.prototype.eats true; // 不整体覆盖constructor 保留这一坑点的实战后果正是本章节配套的另一道习题 4-new-object-same-constructor/task.md 所考察的内容当你不清楚某个obj是由哪个构造函数创建时new obj.constructor()能否正常工作完全取决于该构造函数当时的prototype是否还保有正确的constructor。五、实战启示四条可迁移的工程经验实例的原型在创建时定格任何对F.prototype的整体替换都只影响之后的new F()不要指望它「更新」已有实例修改原型对象的属性是全局生效的由于引用共享F.prototype.xxx ...会被所有既有实例和未来实例同时看到——这既是原型方法共享的基础也是误改原型数据导致「牵一发动全身」的根源delete只删「自己的」属性delete obj.prop删不掉原型上的同名属性想要清理继承属性必须回到原型对象上去删如场景 4或者用Object.defineProperty之类的现代 API 在对象自身遮蔽它整体替换原型要记得补constructor否则obj.constructor将指向Object而非原构造函数基于new obj.constructor()的复用代码会静默产出错误类型的对象。小结本习题通过四次单行改动把F.prototype的两条核心机制——「只在new F时生效」与「Rabbit.prototype与实例[[Prototype]]共享同一对象引用」——以及delete的「对象自身优先」语义全部串了起来。四个答案true / false / true / undefined对应四种截然不同的结局却只源于同一个问题你的操作是在替换指向还是在修改被指向的对象你删除的是实例自己的属性还是原型上的属性。把这四条结论内化于心原型链相关的绝大多数面试题与线上 bug 都能一眼看穿。赞分享文档/教程前端【免费下载链接】en.javascript.infoModern JavaScript Tutorial项目地址https://gitcode.com/gh_mirrors/en/en.javascript.info点击查看免费下载相关推荐JavaScript 数组按引用复制详解为什么 shoppingCart.push() 会改变 fruitsJavaScript 数组按引用复制详解为什么 shoppingCart.push 会改变 fruits 数组是 JavaScript 中用于存储有序集合的文档/教程前端stock 量化交易工具箱上手教程可转债双低选股、LOF 溢价监控与封基轮动回测stock 量化交易工具箱上手教程可转债双低选股、LOF 溢价监控与封基轮动回测 stock 是一个持续更新的 A 股量化交易工具箱内置集思录可转债数据落地金融科技数据分析机器学习JavaScript 闭包变量访问函数读取的是创建处的变量还是调用处的变量——Modern JavaScript Tutorial 例题精讲JavaScript 闭包变量访问函数读取的是创建处的变量还是调用处的变量——Modern JavaScript Tutorial 例题精讲 本篇技术指南围文档/教程前端上一篇Comprehensive Rust 实战深入理解 unsafe 代码块unsafe block——从编译器错误到 SAFETY 注释下一篇Backstage TechDocs 后端扩展点实战指南Preparer、Builds、Publisher 与 Generator 的自定义开发创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表