- 文档/教程
- 前端
【免费下载链接】en.javascript.info
Modern JavaScript Tutorial
导读
在 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 ); // trueRabbit.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()失效的完整链路正是这样的:
- 在
user自身找constructor——没有; - 沿原型链到
User.prototype——也没有(因为被User.prototype = {}覆盖时"忘记"补上); - 再往上到内置
Object.prototype——这里存在Object.prototype.constructor == Object; - 最终执行的是
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
相关推荐
用 new obj.constructor() 克隆同构造器对象:Modern JavaScript Tutorial 的 F.prototype 与 constructor 属性详解
用 new obj.constructor 克隆同构造器对象:Modern JavaScript Tutorial 的 F.prototype 与 constr
文档/教程前端wasm-bindgen 指南:用 `constructor` 属性将 JavaScript 构造函数(`new` 运算符)绑定到 Rust
wasm bindgen 指南:用 constructor 属性将 JavaScript 构造函数( new 运算符)绑定到 Rust wasm_bindgen
开发工具Modern JavaScript Tutorial:深入解析 new Function 动态构造函数的语法、闭包边界与压缩器安全性
Modern JavaScript Tutorial:深入解析 new Function 动态构造函数的语法、闭包边界与压缩器安全性 new Function
文档/教程前端
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考