☰
深入解析 JavaScript 原型继承中的 `this`:从 `rabbit.eat()` 任务看方法查找与执行的分离
2026/10/7 10:42:57 网站建设 项目流程
  • 文档/教程
  • 前端

【免费下载链接】en.javascript.info

Modern JavaScript Tutorial

项目地址:https://gitcode.com/gh_mirrors/en/en.javascript.info
点击查看免费下载

导读

在 JavaScript 的原型继承体系中,一个高频易错点在于:当对象调用从原型链上继承来的方法时,方法内部的this究竟指向谁?本篇以 Modern JavaScript Tutorial(en.javascript.info)中的经典任务Where does it write?(任务原文)为切入点,拆解"属性查找"与"方法执行"两个不同阶段,并借助仓库中的原理图解与配套练习,帮你彻底掌握"this始终是点号前的对象"这一核心结论,从而安全地编写可复用、可共享的"方法库 + 实例对象"架构。

任务回顾:rabbit.eat()把full写到了哪里?

仓库中的 task.md 给出了一段非常精炼的代码:

let animal = { eat() { this.full = true; } }; let rabbit = { __proto__: animal }; rabbit.eat();

任务的问题是:调用rabbit.eat()之后,full属性被写到了animal上还是rabbit上?

直觉上容易踩坑:既然eat方法定义在animal上,this.full = true似乎会修改animal。但正确的答案是——full被写到了rabbit上。

对应的官方解答位于 solution.md,其论证只有三句话,却点破了本质:

答案是rabbit。因为this是点号前的对象,所以rabbit.eat()修改的是rabbit。属性查找与执行是两个不同的事情。rabbit.eat首先在原型上被找到,然后以this=rabbit执行。

核心原理:属性查找与执行是两个阶段

要理解上面的结论,必须把一次方法调用拆成两个独立的阶段:

  1. 查找阶段(lookup):rabbit.eat在rabbit自身对象上没有找到eat,于是沿着[[Prototype]]链向上查找,最终在animal上找到该方法。这一阶段只负责"找到函数",不负责"确定上下文"。
  2. 执行阶段(execution):找到的eat以this = rabbit被调用。this的取值完全由调用方式决定(点号前的对象),与方法定义在哪里无关。

这正是章节正文 article.md 中"The value of this"一节反复强调的规则:

无论方法是在对象自身还是在其原型上被找到,在方法调用中,this始终是点号前的对象。

所以rabbit.eat()等价于把animal.eat这个函数"借"过来,以rabbit为接收者执行。函数体里的this.full = true自然就写到了rabbit上。

图解验证:原型查找的完整链路

仓库中对应的结构图proto-animal-rabbit.svg与proto-animal-rabbit-chain.svg直观展示了查找阶段的行为:读取rabbit.eats时,属性在rabbit上缺失,JavaScript 会顺着[[Prototype]]引用一路向上,在animal中找到它。

同样地,方法调用也遵循这条查找路径,但一旦方法被找到并执行,this便"落地"到发起调用的那个对象上。仓库中的proto-animal-rabbit-walk-3.svg展示了这一写操作的最终效果:rabbit.sleep()执行后,isSleeping属性出现在rabbit上,而作为方法存储的animal始终保持干净:

实战含义:方法共享,状态不共享

这一规则在实践中价值极大,它支撑起了一种非常常见的架构——用一个对象集中存放方法,多个实例对象继承它:

// animal 是"方法存储" let animal = { walk() { if (!this.isSleeping) { alert(`I walk`); } }, sleep() { this.isSleeping = true; } }; let rabbit = { name: "White Rabbit", __proto__: animal }; // 修改的是 rabbit.isSleeping rabbit.sleep(); alert(rabbit.isSleeping); // true alert(animal.isSleeping); // undefined(原型上没有这个属性)

这是 article.md 中的原示例。可以看到:

  • sleep和walk方法定义在animal上,被rabbit复用——方法是共享的;
  • 但rabbit.sleep()内部的this.isSleeping = true写进了rabbit自己的状态——状态是不共享的。

如果有bird、snake等多个对象都继承animal,它们各自调用sleep()时,this都会在调用时刻(点号前)被求值为各自的对象,数据只落入各对象自身。这正是"大对象不会被污染"的保证:animal即使被成千上万个实例继承,也永远只是一个纯粹的方法库。

例外情形:访问器属性(getter/setter)

需要特别指出一个例外:上面的"写操作不经过原型"只对数据属性成立。对于访问器属性(getter/setter),赋值行为等价于调用 setter 函数,因此会触达原型链上的 setter——但this依然遵循"点号前对象"规则。

仓库正文中的经典示例:

let user = { name: "John", surname: "Smith", set fullName(value) { [this.name, this.surname] = value.split(" "); }, get fullName() { return `${this.name} ${this.surname}`; } }; let admin = { __proto__: user, isAdmin: true }; alert(admin.fullName); // John Smith(getter 在原型上被触发) admin.fullName = "Alice Cooper"; // setter 在原型上被触发,但 this=admin alert(admin.fullName); // Alice Cooper,admin 的状态被修改 alert(user.fullName); // John Smith,user 的状态保持原样

admin.fullName = "Alice Cooper"这行赋值,虽然 setter 函数体定义在user上,但由于调用点是admin,this仍是admin,于是admin.name、admin.surname被修改,而user毫发无损。仓库中的proto-user-admin.svg正是对这一场景的结构化呈现。

配套练习:把"查找"与"写入"彻底分开理解

同一章节下还有两个练习,恰好从不同侧面强化本节主题。

练习一:delete 与原型查找(1-property-after-delete)

let animal = { jumps: null }; let rabbit = { __proto__: animal, jumps: true }; alert(rabbit.jumps); // ? (1) → true,来自 rabbit 自身 delete rabbit.jumps; alert(rabbit.jumps); // ? (2) → null,来自 animal delete animal.jumps; alert(rabbit.jumps); // ? (3) → undefined,链上已无此属性

这个练习印证了"读操作会沿原型链查找,而写/删操作只作用于对象自身":delete rabbit.jumps删除的是rabbit自己的属性,随后读取再次触发原型查找,在animal上找到null;再删除animal.jumps后,整条链上都不存在该属性,结果为undefined。

练习二:搜索算法与性能(2-search-algorithm)

构建一条pockets -> bed -> table -> head的原型链:

let head = { glasses: 1 }; let table = { pen: 3, __proto__: head }; let bed = { sheet: 1, pillow: 2, __proto__: table }; let pockets = { money: 2000, __proto__: bed }; alert(pockets.pen); // 3,在 table 中找到 alert(bed.glasses); // 1,在 head 中找到 alert(table.money); // undefined,链上没有

关于"pockets.glasses和head.glasses哪个更快"的问题,官方解答明确指出:在现代引擎中两者没有性能差异——引擎会记住属性在链上的查找位置并在下次请求时直接复用,同时会在属性变化时智能更新内部缓存。这从侧面说明:原型链查找虽然跨越多个对象,但并不会带来可感知的开销,真正的语义难点始终是this的绑定。

关键要点速查

  • 查找与执行分离:rabbit.eat()中,eat在原型上被找到,但以this = rabbit执行(solution.md)。
  • this永远指向点号前的对象:无论方法定义在对象自身还是原型上(article.md 的"The value of this"一节)。
  • 方法共享、状态隔离:用原型集中存放方法,实例各自维护状态,是"方法库 + 实例"架构的正确姿势。
  • 访问器属性是例外:赋值会触发原型链上的 setter,但this依旧是调用点对象。
  • 写/删操作不走原型:只有读操作会沿[[Prototype]]链查找;delete只删除对象自身属性。

理解了这个任务,就拿到了解读一切原型继承代码的钥匙:看到obj.method(),先想"方法从哪来",再想"this是谁"——前者决定能否调用,后者决定改谁的状态。

  • 文档/教程
  • 前端

【免费下载链接】en.javascript.info

Modern JavaScript Tutorial

项目地址:https://gitcode.com/gh_mirrors/en/en.javascript.info
点击查看免费下载

相关推荐

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询