news 2026/10/6 12:13:03

Modern JavaScript Tutorial 深度解析:构造函数 `F.prototype`、`new` 与 `constructor` 属性的完整指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Modern JavaScript Tutorial 深度解析:构造函数 `F.prototype`、`new` 与 `constructor` 属性的完整指南
  • 文档/教程
  • 前端

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

Modern JavaScript Tutorial

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

导读

在 JavaScript 中,通过构造函数new F()创建对象时,F.prototype属性决定了新对象的内部[[Prototype]]指向——这是语言中最古老、使用最广泛的设置原型的方式,也是理解后续class语法与原型链机制的地基。本指南以 Modern JavaScript Tutorial 的 F.prototype 章节 为主体,结合同章节配套习题与 构造函数与 new、原型继承 等仓库章节,系统讲解F.prototype的工作时机、默认constructor属性的行为、替换原型带来的陷阱与正确修复方式,并给出可直接运行的验证代码。读完本文,你将能准确回答"new是如何借助prototype设置继承关系的""为什么覆盖Rabbit.prototype后rabbit.constructor会失效"以及"如何安全地利用constructor创建同类型新对象"等问题。

一、从[[Prototype]]说起:构造函数如何参与原型继承

在 原型继承章节 中我们已知:每个 JavaScript 对象都有一个隐藏的内部属性[[Prototype]],它要么是null,要么指向另一个对象(称为"原型")。当读取对象上不存在的属性时,JavaScript 会自动沿[[Prototype]]链向上查找,这就是"原型式继承"。

而通过构造函数创建对象时,new操作符恰好会利用构造函数上的一个普通属性prototype来为新对象指定[[Prototype]]。也就是说:

如果F.prototype是一个对象,那么new F()创建的新对象,其[[Prototype]]会被设置为F.prototype所指向的对象。

这里的F.prototype并不是什么特殊语法,它就是挂在构造函数F上的一个名为"prototype"的普通属性——注意不要与内部属性[[Prototype]]混淆,二者是完全不同的东西。

基础示例:让所有兔子继承动物

let animal = { eats: true }; function Rabbit(name) { this.name = name; } Rabbit.prototype = animal; // 关键赋值 let rabbit = new Rabbit("White Rabbit"); // rabbit.__proto__ == animal alert( rabbit.eats ); // true

Rabbit.prototype = animal这句话的字面含义是:"当创建一个new Rabbit时,把它的[[Prototype]]赋值为animal"。于是rabbit自身没有eats属性,但可以通过原型链从animal上读到它:

图中水平箭头表示"prototype"是一个普通属性,垂直箭头表示rabbit对animal的继承关系(即[[Prototype]]引用)。这正是理解本节的关键心智模型:构造函数通过一个普通属性,把继承关系"注入"到每次new创建的新对象上。

历史背景:为什么老代码大量依赖它

值得强调的是,原型式继承从 JavaScript 诞生起就是语言核心特性之一。但在早期,开发者并没有直接访问[[Prototype]]的途径,唯一可靠的做法就是本文描述的——构造函数上的"prototype"属性。因此直到今天,仍有大量现存脚本沿用这一模式。这正是我们有必要透彻理解它的原因。(现代的Object.create、Object.setPrototypeOf等替代方案在 原型方法章节 中有专门讲解。)

关键时机:F.prototype只在new F()调用的那一刻起作用

F.prototype属性仅在调用new F时被读取,用来为新对象设置[[Prototype]]。它不是一个"持续生效"的引用:

function Rabbit() {} Rabbit.prototype = { eats: true }; let rabbit1 = new Rabbit(); // 此时 rabbit1 的 [[Prototype]] 是 { eats: true } Rabbit.prototype = { eats: false, jumps: true }; // 之后才改写 prototype let rabbit2 = new Rabbit(); // rabbit2 的 [[Prototype]] 是 { eats: false, jumps: true } alert(rabbit1.eats); // true —— 旧对象保留旧的 [[Prototype]] alert(rabbit2.eats); // false —— 新对象获得新的 [[Prototype]]

也就是说:已创建的对象不受后续F.prototype改写的任何影响,只有之后新建的对象会使用新值。这一点在后面的习题验证中还会反复出现。

二、默认的F.prototype与constructor属性

每个函数(包括普通函数)都天然拥有"prototype"属性,即使我们完全没有提供它。默认的"prototype"是一个只含一个属性constructor的对象,且constructor指回函数自身:

function Rabbit() {} /* 默认 prototype 相当于: Rabbit.prototype = { constructor: Rabbit }; */

可以直接验证:

function Rabbit() {} // 默认情况下: // Rabbit.prototype = { constructor: Rabbit } alert( Rabbit.prototype.constructor == Rabbit ); // true

如果我们什么都不做,那么所有通过new Rabbit()创建的对象,都会通过[[Prototype]]继承到这个constructor属性:

function Rabbit() {} // 默认情况下: // Rabbit.prototype = { constructor: Rabbit } let rabbit = new Rabbit(); // 继承自 {constructor: Rabbit} alert(rabbit.constructor == Rabbit); // true(来自原型)

利用constructor创建同类型对象

constructor属性的实用价值在于:当我们手里有一个对象、却不知道它是用哪个构造函数创建时(例如来自第三方库),可以通过obj.constructor再造一个同类对象:

function Rabbit(name) { this.name = name; alert(name); } let rabbit = new Rabbit("White Rabbit"); let rabbit2 = new rabbit.constructor("Black Rabbit"); // 等价于 new Rabbit("Black Rabbit")

这正是 配套习题 "Create an object with the same constructor" 探讨的场景:new obj.constructor()是否可靠,完全取决于constructor属性是否仍指向正确的构造函数。只要没有覆盖过默认"prototype",它就必然可靠:

function User(name) { this.name = name; } let user = new User('John'); let user2 = new user.constructor('Pete'); alert( user2.name ); // Pete(成功!)

它之所以成功,是因为User.prototype.constructor == User(见 该习题解答)。

三、最大的坑:JavaScript 并不保证constructor始终正确

JavaScript 自身并不保证"constructor"值始终正确。它只是默认出现在函数的"prototype"里,仅此而已。之后这个属性会变成什么样,完全取决于我们自己。

尤其是:如果整体替换掉默认的prototype,新的 prototype 对象里就不会再有constructor了。例如:

function Rabbit() {} Rabbit.prototype = { jumps: true }; let rabbit = new Rabbit(); alert(rabbit.constructor === Rabbit); // false

此时rabbit沿原型链查到的constructor是更上层Object.prototype.constructor(即内置的Object),而不是Rabbit。习题 Changing "prototype" 与解答 solution.md 里,new obj.constructor()失效的完整链路正是这样的:

  1. 在user自身找constructor——没有;
  2. 沿原型链到User.prototype——也没有(因为被User.prototype = {}覆盖时"忘记"补上);
  3. 再往上到内置Object.prototype——这里存在Object.prototype.constructor == Object;
  4. 最终执行的是new Object('Pete'),name自然成了undefined。

也就是说,丢失constructor的后果是:同类型对象的"复制"静默地变成了普通Object创建,代码不报错但结果完全错误。

四、两种保住constructor的正确姿势

既然整块替换会丢失constructor,官方教程给出了两个修复方案:

方案一:不要整体覆盖,只增删默认 prototype 的属性

保留默认prototype对象(其中含constructor),只往上面添加/删除属性:

function Rabbit() {} // 不要整体覆盖 Rabbit.prototype // 只往上面添加属性 Rabbit.prototype.jumps = true // 默认的 Rabbit.prototype.constructor 得以保留

方案二:整体覆盖时,手动补回constructor

如果确实需要用一个全新的对象替换prototype,记得把constructor一并写进去:

Rabbit.prototype = { jumps: true, constructor: Rabbit }; // 因为手动补上了 constructor,现在它也正确

两种方式都能保证rabbit.constructor === Rabbit,从而让new obj.constructor()这类代码继续可靠工作。

五、配套习题实战:改写prototype的四种情形验证

仓库中 1-changing-prototype 习题从四个角度测试对F.prototype语义的理解,其完整解答见 solution.md。初始代码如下:

function Rabbit() {} Rabbit.prototype = { eats: true }; let rabbit = new Rabbit(); alert( rabbit.eats ); // true

情形 1:整体改写Rabbit.prototype = {}

function Rabbit() {} Rabbit.prototype = { eats: true }; let rabbit = new Rabbit(); Rabbit.prototype = {}; // 改写发生在 new 之后 alert( rabbit.eats ); // ?

答案:true。Rabbit.prototype的赋值只对之后新建的对象设置[[Prototype]],对已存在的rabbit毫无影响——它的[[Prototype]]仍是旧对象{ eats: true }。

情形 2:修改Rabbit.prototype.eats = false

function Rabbit() {} Rabbit.prototype = { eats: true }; let rabbit = new Rabbit(); Rabbit.prototype.eats = false; alert( rabbit.eats ); // ?

答案:false。对象是按引用赋值的。Rabbit.prototype指向的对象并没有被复制,rabbit的[[Prototype]]与Rabbit.prototype引用的是同一个对象。因此通过任一引用修改其内容,另一引用都会看到变化。

情形 3:delete rabbit.eats

function Rabbit() {} Rabbit.prototype = { eats: true }; let rabbit = new Rabbit(); delete rabbit.eats; alert( rabbit.eats ); // ?

答案:true。所有delete操作都直接作用于对象自身。rabbit自己没有eats属性(它只在原型上),所以删除操作不产生任何效果,属性仍可从原型读到。(这正是 原型继承章节 强调的"写/删操作不走原型"原则。)

情形 4:delete Rabbit.prototype.eats

function Rabbit() {} Rabbit.prototype = { eats: true }; let rabbit = new Rabbit(); delete Rabbit.prototype.eats; alert( rabbit.eats ); // ?

答案:undefined。这次删除的是原型对象上的eats属性,它从整个原型链中消失,因此rabbit.eats再也查不到,返回undefined。

这四个情形共同印证了第三节的核心结论:F.prototype只在new的瞬间被消费一次;对象内容按引用共享;读写删操作只在对象自身生效。它们是排查原型相关 bug 时最有价值的判断依据。

六、总结:三条必须记住的规则

本节内容看似简单,但包含几条极易踩坑的要点,官方教程的 Summary 将其凝练为:

  • F.prototype(不要与[[Prototype]]混淆)会在new F()被调用时,为新对象的[[Prototype]]赋值。它只在构造函数配合new使用时才有这种特殊效果。
  • F.prototype的值必须是对象或null,其他值不会生效。这也与原型继承章节中"[[Prototype]]只能是对象或null"的约束一脉相承。
  • 默认情况下所有函数都有F.prototype = { constructor: F },因此可以通过对象的"constructor"属性拿到它的构造函数。但一旦整体覆盖prototype而不补回constructor,这个能力就会静默丢失。

最后还要强调一个易混淆点:在普通对象上,prototype毫无特殊之处,它只是一个普通属性名:

let user = { name: "John", prototype: "Bla-bla" // 没有任何魔法 };

只有挂在构造函数上、并经由new调用时,prototype才承担起设置[[Prototype]]的职责。

七、延伸阅读

  • 构造函数与 operator "new":理解new的完整执行流程(创建空对象、绑定this、返回this)与本节的直接前置知识。
  • 原型继承(Prototypal inheritance):[[Prototype]]、原型链查找与this语义的完整介绍。
  • 原型方法,没有proto的对象:现代替代方案Object.getPrototypeOf / setPrototypeOf / create与原型历史的演进脉络。
  • 原生原型(Native prototypes):内置对象(如Object.prototype)的constructor如何在实践中影响上面的查找链路。
  • 配套习题与解答:Changing "prototype"、Create an object with the same constructor。

理解F.prototype之后,后续学习 class 语法 与更高级的继承模式会顺畅得多——class正是建立在这套原型机制之上的语法糖。

  • 文档/教程
  • 前端

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

Modern JavaScript Tutorial

项目地址:https://gitcode.com/gh_mirrors/en/en.javascript.info
点击查看免费下载
上一篇:wxBot代码规范与最佳实践:编写高质量机器人代码的完整指南
下一篇:MLKit Android机器学习工具包:终极集成与最佳实践指南

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

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

CLIP改进工作串讲(上)

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

作者头像 李华
网站建设 2026/10/6 12:04:39

仪表读数识别技术拆解:从YOLO检测到椭圆拟合的完整方案

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

作者头像 李华
网站建设 2026/10/6 12:00:36

PCIe Polling.Compliance进入与pattern生成:合规性测试避坑指南

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

作者头像 李华
网站建设 2026/10/6 12:00:34

ESP-IDF调试报错No match:GDB通配符展开与cwd路径陷阱解析

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

作者头像 李华
网站建设 2026/10/6 11:59:34

泛微E9统一集成待办中心接口对接:推送、更新与办结实战

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

作者头像 李华
网站建设 2026/10/6 11:59:21

PW2312实战:24V转3.3V同步降压电路完整设计指南

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

作者头像 李华