造函數(shù) `F.prototype`、`new` 與 `constructor` 屬性的完整指南)
文檔/教程前端【免費下載鏈接】en.javascript.infoModern JavaScript Tutorial項目地址https://gitcode.com/gh_mirrors/en/en.javascript.info點擊查看免費下載導(dǎo)讀在 JavaScript 中通過構(gòu)造函數(shù)new F()創(chuàng)建對象時F.prototype屬性決定了新對象的內(nèi)部[[Prototype]]指向——這是語言中最古老、使用最廣泛的設(shè)置原型的方式也是理解后續(xù)class語法與原型鏈機制的地基。本指南以 Modern JavaScript Tutorial 的 F.prototype 章節(jié) 為主體結(jié)合同章節(jié)配套習(xí)題與 構(gòu)造函數(shù)與 new、原型繼承 等倉庫章節(jié)系統(tǒng)講解F.prototype的工作時機、默認constructor屬性的行為、替換原型帶來的陷阱與正確修復(fù)方式并給出可直接運行的驗證代碼。讀完本文你將能準確回答new是如何借助prototype設(shè)置繼承關(guān)系的為什么覆蓋Rabbit.prototype后rabbit.constructor會失效以及如何安全地利用constructor創(chuàng)建同類型新對象等問題。一、從[[Prototype]]說起構(gòu)造函數(shù)如何參與原型繼承在 原型繼承章節(jié) 中我們已知每個 JavaScript 對象都有一個隱藏的內(nèi)部屬性[[Prototype]]它要么是null要么指向另一個對象稱為原型。當讀取對象上不存在的屬性時JavaScript 會自動沿[[Prototype]]鏈向上查找這就是原型式繼承。而通過構(gòu)造函數(shù)創(chuàng)建對象時new操作符恰好會利用構(gòu)造函數(shù)上的一個普通屬性prototype來為新對象指定[[Prototype]]。也就是說如果F.prototype是一個對象那么new F()創(chuàng)建的新對象其[[Prototype]]會被設(shè)置為F.prototype所指向的對象。這里的F.prototype并不是什么特殊語法它就是掛在構(gòu)造函數(shù)F上的一個名為prototype的普通屬性——注意不要與內(nèi)部屬性[[Prototype]]混淆二者是完全不同的東西?;A(chǔ)示例讓所有兔子繼承動物let animal { eats: true }; function Rabbit(name) { this.name name; } Rabbit.prototype animal; // 關(guān)鍵賦值 let rabbit new Rabbit(White Rabbit); // rabbit.__proto__ animal alert( rabbit.eats ); // trueRabbit.prototype animal這句話的字面含義是當創(chuàng)建一個new Rabbit時把它的[[Prototype]]賦值為animal。于是rabbit自身沒有eats屬性但可以通過原型鏈從animal上讀到它圖中水平箭頭表示prototype是一個普通屬性垂直箭頭表示rabbit對animal的繼承關(guān)系即[[Prototype]]引用。這正是理解本節(jié)的關(guān)鍵心智模型構(gòu)造函數(shù)通過一個普通屬性把繼承關(guān)系注入到每次new創(chuàng)建的新對象上。歷史背景為什么老代碼大量依賴它值得強調(diào)的是原型式繼承從 JavaScript 誕生起就是語言核心特性之一。但在早期開發(fā)者并沒有直接訪問[[Prototype]]的途徑唯一可靠的做法就是本文描述的——構(gòu)造函數(shù)上的prototype屬性。因此直到今天仍有大量現(xiàn)存腳本沿用這一模式。這正是我們有必要透徹理解它的原因?,F(xiàn)代的Object.create、Object.setPrototypeOf等替代方案在 原型方法章節(jié) 中有專門講解。關(guān)鍵時機F.prototype只在new F()調(diào)用的那一刻起作用F.prototype屬性僅在調(diào)用new F時被讀取用來為新對象設(shè)置[[Prototype]]。它不是一個持續(xù)生效的引用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]]也就是說已創(chuàng)建的對象不受后續(xù)F.prototype改寫的任何影響只有之后新建的對象會使用新值。這一點在后面的習(xí)題驗證中還會反復(fù)出現(xiàn)。二、默認的F.prototype與constructor屬性每個函數(shù)包括普通函數(shù)都天然擁有prototype屬性即使我們完全沒有提供它。默認的prototype是一個只含一個屬性constructor的對象且constructor指回函數(shù)自身function Rabbit() {} /* 默認 prototype 相當于 Rabbit.prototype { constructor: Rabbit }; */可以直接驗證function Rabbit() {} // 默認情況下 // Rabbit.prototype { constructor: Rabbit } alert( Rabbit.prototype.constructor Rabbit ); // true如果我們什么都不做那么所有通過new Rabbit()創(chuàng)建的對象都會通過[[Prototype]]繼承到這個constructor屬性function Rabbit() {} // 默認情況下 // Rabbit.prototype { constructor: Rabbit } let rabbit new Rabbit(); // 繼承自 {constructor: Rabbit} alert(rabbit.constructor Rabbit); // true來自原型利用constructor創(chuàng)建同類型對象constructor屬性的實用價值在于當我們手里有一個對象、卻不知道它是用哪個構(gòu)造函數(shù)創(chuàng)建時例如來自第三方庫可以通過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)這正是 配套習(xí)題 Create an object with the same constructor 探討的場景new obj.constructor()是否可靠完全取決于constructor屬性是否仍指向正確的構(gòu)造函數(shù)。只要沒有覆蓋過默認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見 該習(xí)題解答。三、最大的坑JavaScript 并不保證constructor始終正確JavaScript 自身并不保證constructor值始終正確。它只是默認出現(xiàn)在函數(shù)的prototype里僅此而已。之后這個屬性會變成什么樣完全取決于我們自己。尤其是如果整體替換掉默認的prototype新的 prototype 對象里就不會再有constructor了。例如function Rabbit() {} Rabbit.prototype { jumps: true }; let rabbit new Rabbit(); alert(rabbit.constructor Rabbit); // false此時rabbit沿原型鏈查到的constructor是更上層Object.prototype.constructor即內(nèi)置的Object而不是Rabbit。習(xí)題 Changing prototype 與解答 solution.md 里new obj.constructor()失效的完整鏈路正是這樣的在user自身找constructor——沒有沿原型鏈到User.prototype——也沒有因為被User.prototype {}覆蓋時忘記補上再往上到內(nèi)置Object.prototype——這里存在Object.prototype.constructor Object最終執(zhí)行的是new Object(Pete)name自然成了undefined。也就是說丟失constructor的后果是同類型對象的復(fù)制靜默地變成了普通Object創(chuàng)建代碼不報錯但結(jié)果完全錯誤。四、兩種保住constructor的正確姿勢既然整塊替換會丟失constructor官方教程給出了兩個修復(fù)方案方案一不要整體覆蓋只增刪默認 prototype 的屬性保留默認prototype對象其中含constructor只往上面添加/刪除屬性function Rabbit() {} // 不要整體覆蓋 Rabbit.prototype // 只往上面添加屬性 Rabbit.prototype.jumps true // 默認的 Rabbit.prototype.constructor 得以保留方案二整體覆蓋時手動補回constructor如果確實需要用一個全新的對象替換prototype記得把constructor一并寫進去Rabbit.prototype { jumps: true, constructor: Rabbit }; // 因為手動補上了 constructor現(xiàn)在它也正確兩種方式都能保證rabbit.constructor Rabbit從而讓new obj.constructor()這類代碼繼續(xù)可靠工作。五、配套習(xí)題實戰(zhàn)改寫prototype的四種情形驗證倉庫中 1-changing-prototype 習(xí)題從四個角度測試對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 {}; // 改寫發(fā)生在 new 之后 alert( rabbit.eats ); // 答案true。Rabbit.prototype的賦值只對之后新建的對象設(shè)置[[Prototype]]對已存在的rabbit毫無影響——它的[[Prototype]]仍是舊對象{ eats: true }。情形 2修改Rabbit.prototype.eats falsefunction Rabbit() {} Rabbit.prototype { eats: true }; let rabbit new Rabbit(); Rabbit.prototype.eats false; alert( rabbit.eats ); // 答案false。對象是按引用賦值的。Rabbit.prototype指向的對象并沒有被復(fù)制rabbit的[[Prototype]]與Rabbit.prototype引用的是同一個對象。因此通過任一引用修改其內(nèi)容另一引用都會看到變化。情形 3delete rabbit.eatsfunction Rabbit() {} Rabbit.prototype { eats: true }; let rabbit new Rabbit(); delete rabbit.eats; alert( rabbit.eats ); // 答案true。所有delete操作都直接作用于對象自身。rabbit自己沒有eats屬性它只在原型上所以刪除操作不產(chǎn)生任何效果屬性仍可從原型讀到。這正是 原型繼承章節(jié) 強調(diào)的寫/刪操作不走原型原則。情形 4delete Rabbit.prototype.eatsfunction Rabbit() {} Rabbit.prototype { eats: true }; let rabbit new Rabbit(); delete Rabbit.prototype.eats; alert( rabbit.eats ); // 答案undefined。這次刪除的是原型對象上的eats屬性它從整個原型鏈中消失因此rabbit.eats再也查不到返回undefined。這四個情形共同印證了第三節(jié)的核心結(jié)論F.prototype只在new的瞬間被消費一次對象內(nèi)容按引用共享讀寫刪操作只在對象自身生效。它們是排查原型相關(guān) bug 時最有價值的判斷依據(jù)。六、總結(jié)三條必須記住的規(guī)則本節(jié)內(nèi)容看似簡單但包含幾條極易踩坑的要點官方教程的 Summary 將其凝練為F.prototype不要與[[Prototype]]混淆會在new F()被調(diào)用時為新對象的[[Prototype]]賦值。它只在構(gòu)造函數(shù)配合new使用時才有這種特殊效果。F.prototype的值必須是對象或null其他值不會生效。這也與原型繼承章節(jié)中[[Prototype]]只能是對象或null的約束一脈相承。默認情況下所有函數(shù)都有F.prototype { constructor: F }因此可以通過對象的constructor屬性拿到它的構(gòu)造函數(shù)。但一旦整體覆蓋prototype而不補回constructor這個能力就會靜默丟失。最后還要強調(diào)一個易混淆點在普通對象上prototype毫無特殊之處它只是一個普通屬性名let user { name: John, prototype: Bla-bla // 沒有任何魔法 };只有掛在構(gòu)造函數(shù)上、并經(jīng)由new調(diào)用時prototype才承擔起設(shè)置[[Prototype]]的職責(zé)。七、延伸閱讀構(gòu)造函數(shù)與 operator new理解new的完整執(zhí)行流程創(chuàng)建空對象、綁定this、返回this與本節(jié)的直接前置知識。原型繼承Prototypal inheritance[[Prototype]]、原型鏈查找與this語義的完整介紹。原型方法沒有proto的對象現(xiàn)代替代方案Object.getPrototypeOf / setPrototypeOf / create與原型歷史的演進脈絡(luò)。原生原型Native prototypes內(nèi)置對象如Object.prototype的constructor如何在實踐中影響上面的查找鏈路。配套習(xí)題與解答Changing prototype、Create an object with the same constructor。理解F.prototype之后后續(xù)學(xué)習(xí) class 語法 與更高級的繼承模式會順暢得多——class正是建立在這套原型機制之上的語法糖。贊分享文檔/教程前端【免費下載鏈接】en.javascript.infoModern JavaScript Tutorial項目地址https://gitcode.com/gh_mirrors/en/en.javascript.info點擊查看免費下載相關(guān)推薦用 new obj.constructor() 克隆同構(gòu)造器對象Modern JavaScript Tutorial 的 F.prototype 與 constructor 屬性詳解用 new obj.constructor 克隆同構(gòu)造器對象Modern JavaScript Tutorial 的 F.prototype 與 constr文檔/教程前端wasm-bindgen 指南用 constructor 屬性將 JavaScript 構(gòu)造函數(shù)new 運算符綁定到 Rustwasm bindgen 指南用 constructor 屬性將 JavaScript 構(gòu)造函數(shù) new 運算符綁定到 Rust wasm_bindgen開發(fā)工具Modern JavaScript Tutorial深入解析 new Function 動態(tài)構(gòu)造函數(shù)的語法、閉包邊界與壓縮器安全性Modern JavaScript Tutorial深入解析 new Function 動態(tài)構(gòu)造函數(shù)的語法、閉包邊界與壓縮器安全性 new Function文檔/教程前端上一篇wxBot代碼規(guī)范與最佳實踐編寫高質(zhì)量機器人代碼的完整指南下一篇MLKit Android機器學(xué)習(xí)工具包終極集成與最佳實踐指南創(chuàng)作聲明:本文部分內(nèi)容由AI輔助生成(AIGC),僅供參考