用 super()——以 zh.javascript.info 的“創(chuàng)建實例時出錯”為例)
文檔教程前端【免費下載鏈接】zh.javascript.info現(xiàn)代 JavaScript 教程The Modern JavaScript Tutorial以最新的 ECMAScript 規(guī)范為基準(zhǔn)通過簡單但足夠詳細(xì)的內(nèi)容為你講解從基礎(chǔ)到高階的 JavaScript 相關(guān)知識。項目地址https://gitcode.com/gh_mirrors/zh/zh.javascript.info點擊查看免費下載在 JavaScript 中當(dāng)一個類通過extends繼承另一個類并重寫了自己的constructor時new一個實例往往會拋出Error: this is not defined之類的錯誤。本指南以現(xiàn)代 JavaScript 教程zh.javascript.info中 類繼承一章 的經(jīng)典習(xí)題“創(chuàng)建實例時出錯”為線索從題目、錯誤現(xiàn)象出發(fā)深入剖析派生構(gòu)造器derived constructor的特殊內(nèi)部機制給出標(biāo)準(zhǔn)修正方案并結(jié)合倉庫內(nèi)的ExtendedClock實戰(zhàn)任務(wù)演示真實項目中如何正確書寫子類構(gòu)造器。讀完本文你將徹底理解“子類 constructor 必須先調(diào)用super()才能使用this”這條鐵律的底層原因并能熟練寫出健壯的繼承類。題目重現(xiàn)無法創(chuàng)建的 Rabbit 對象倉庫中的 task.md 給出了一段看似合理、實際卻無法運行的代碼class Animal { constructor(name) { this.name name; } } class Rabbit extends Animal { constructor(name) { this.name name; this.created Date.now(); } } let rabbit new Rabbit(White Rabbit); // Error: this is not defined alert(rabbit.name);運行new Rabbit(White Rabbit)時JavaScript 引擎直接拋出Error: this is not defined。從直覺上看Rabbit的構(gòu)造器只是在重復(fù)Animal構(gòu)造器的邏輯設(shè)置this.name再額外記錄created時間戳為何會報錯關(guān)鍵點在于繼承類的 constructor 必須調(diào)用super(...)并且必須在任何this訪問之前調(diào)用。這份代碼沒有調(diào)用super()所以引擎無法為Rabbit實例創(chuàng)建this對象任何對this的讀寫都無從談起。根因剖析派生構(gòu)造器的[[ConstructorKind]]: derived為什么普通類不需要super()而繼承類必須要類繼承主教程 給出了規(guī)范的級解釋在 JavaScript 內(nèi)部繼承類的構(gòu)造函數(shù)即“派生構(gòu)造器”derived constructor與其他函數(shù)存在本質(zhì)區(qū)別——它帶有一個特殊的內(nèi)部標(biāo)簽[[ConstructorKind]]: derived。這個標(biāo)簽直接改變new的行為構(gòu)造器類型new時的行為是否需要super()常規(guī)函數(shù) / 基類構(gòu)造器引擎自動創(chuàng)建一個空對象并賦值給this不需要派生構(gòu)造器extends的子類不創(chuàng)建對象不初始化this期望父類構(gòu)造器來完成必須調(diào)用super()也就是說當(dāng)通過new執(zhí)行一個派生構(gòu)造器時JavaScript 引擎不會像對待普通函數(shù)那樣先創(chuàng)建一個空對象并綁定為this而是把創(chuàng)建實例對象的任務(wù)“移交”給父類構(gòu)造器。super(...)的作用就是執(zhí)行父類base的 constructor讓它完成this對象的創(chuàng)建與初始化。如果跳過super()this就始終處于未定義狀態(tài)一旦代碼嘗試訪問this.name便會立刻報錯。無 constructor 時引擎自動補全主教程還特別指出如果一個繼承類沒有聲明自己的constructor根據(jù) ECMAScript 規(guī)范引擎會自動生成如下“空”構(gòu)造器class Rabbit extends Animal { // 為沒有自己的 constructor 的擴展類生成的 constructor(...args) { super(...args); } }它會調(diào)用父類構(gòu)造器并透傳全部參數(shù)這也是為什么不寫 constructor 時繼承一切正常的原因。問題只出現(xiàn)在子類自己聲明了 constructor 卻忘了調(diào)用super()的場景。修正方案先super()再使用this倉庫中 solution.md 給出了標(biāo)準(zhǔn)修正核心就是在訪問this之前先調(diào)用super(name)class Animal { constructor(name) { this.name name; } } class Rabbit extends Animal { constructor(name) { super(name); // 先調(diào)用父類構(gòu)造器創(chuàng)建并初始化 this this.created Date.now(); // 然后才能使用 this } } let rabbit new Rabbit(White Rabbit); // 現(xiàn)在好了 alert(rabbit.name); // White Rabbit修正后的執(zhí)行流程是new Rabbit(White Rabbit)進入派生構(gòu)造器此時this尚未創(chuàng)建super(name)調(diào)用Animal的 constructor由它完成this的創(chuàng)建并執(zhí)行this.name name控制權(quán)返回子類構(gòu)造器此時this已可用再執(zhí)行this.created Date.now()最終rabbit實例同時擁有name和created兩個屬性。這條規(guī)則同樣適用于帶多個參數(shù)、帶默認(rèn)值或解構(gòu)參數(shù)的構(gòu)造器super()必須是子類構(gòu)造器中第一次觸碰this之前的調(diào)用順序不可顛倒。深入理解字段初始化順序與重寫字段的陷阱理解了super()的必需性后還有一個容易踩坑的衍生知識點——類字段的初始化順序。主教程用一個例子揭示了繼承體系下字段與方法行為的差異class Animal { name animal; constructor() { alert(this.name); // (*) } } class Rabbit extends Animal { name rabbit; } new Animal(); // animal new Rabbit(); // animal而不是 rabbit字段初始化的規(guī)則是基類未繼承任何類的類字段在構(gòu)造器調(diào)用之前初始化派生類字段在super()返回之后立刻初始化。因此當(dāng)Rabbit沒有自己的 constructor等價于空構(gòu)造器super(...args)時new Rabbit()會先執(zhí)行Animal的構(gòu)造器而此刻Rabbit的name rabbit字段尚未初始化所以父類構(gòu)造器讀到的永遠(yuǎn)是父類自己的字段值animal。作為對比若父類構(gòu)造器調(diào)用的是this.showName()方法由于方法查找沿原型鏈進行子類重寫的方法會被命中輸出就會是rabbit。這就是 JavaScript 中“字段按值綁定、方法按原型鏈解析”的微妙差異。若業(yè)務(wù)上必須讓父類構(gòu)造器感知子類字段建議改用方法或 getter/setter 而非類字段。實戰(zhàn)驗證用super()正確擴展 Clock 類倉庫中緊鄰的第二個任務(wù) 擴展 clock 是super()在實際代碼中的典型用法在不修改父類的前提下擴展其能力。父類 clock.js 每秒打印一次時間其 constructor 接收一個解構(gòu)參數(shù){ template }class Clock { constructor({ template }) { this.template template; } // render / stop / start ... }子類 extended-clock.js 新增precision兩次 tick 之間的毫秒數(shù)默認(rèn) 1000構(gòu)造器先調(diào)用super(options)完成父類初始化再設(shè)置自己的字段class ExtendedClock extends Clock { constructor(options) { super(options); let { precision 1000 } options; this.precision precision; } start() { this.render(); this.timer setInterval(() this.render(), this.precision); } };注意這里三個關(guān)鍵點super(options)把完整參數(shù)對象透傳給父類構(gòu)造器父類再解構(gòu)出template子類自己從options中解構(gòu)出precision并給出默認(rèn)值1000子類重寫了start()將setInterval的間隔從硬編碼的1000改為this.precision實現(xiàn)了可配置刷新頻率運行入口 index.html 演示了實際使用傳入precision: 10000創(chuàng)建每 10 秒才刷新一次的低分辨率時鐘。這個例子清晰展示了正確書寫派生構(gòu)造器的完整模式先super(父類所需參數(shù))再初始化子類自有狀態(tài)。小結(jié)與自檢清單回到“創(chuàng)建實例時出錯”這道題可以總結(jié)為如下要點繼承類的 constructor 必須調(diào)用super(...)且調(diào)用必須發(fā)生在任何this讀寫之前根本原因是派生構(gòu)造器帶有內(nèi)部標(biāo)簽[[ConstructorKind]]: derivednew時不會自動創(chuàng)建this對象該任務(wù)被移交給父類構(gòu)造器子類未聲明 constructor 時引擎會自動生成constructor(...args) { super(...args); }派生類的類字段在super()之后才初始化父類構(gòu)造器內(nèi)訪問重寫字段會讀到父類值必要時改用方法或 getter/setter實戰(zhàn)中如ExtendedClock保持“super(options)透傳 → 子類自有狀態(tài)初始化 → 重寫方法”的書寫順序即可避免此類錯誤。對照代碼排查時只需檢查子類構(gòu)造器第一行之前是否出現(xiàn)了this.xxx ...如果出現(xiàn)了就說明super()被放在了錯誤的位置——把super()提到最前問題即告解決。贊分享文檔教程前端【免費下載鏈接】zh.javascript.info現(xiàn)代 JavaScript 教程The Modern JavaScript Tutorial以最新的 ECMAScript 規(guī)范為基準(zhǔn)通過簡單但足夠詳細(xì)的內(nèi)容為你講解從基礎(chǔ)到高階的 JavaScript 相關(guān)知識。項目地址https://gitcode.com/gh_mirrors/zh/zh.javascript.info點擊查看免費下載相關(guān)推薦JavaScript 類繼承中重寫 constructor 為何報錯 this is not definedsuper() 調(diào)用的必要性與底層原理JavaScript 類繼承中重寫 constructor 為何報錯 this is not definedsuper 調(diào)用的必要性與底層原理 本指南基于文檔教程前端JavaScript 類繼承派生類構(gòu)造函數(shù)必須調(diào)用 super() 的原因與修復(fù)JavaScript 類繼承派生類構(gòu)造函數(shù)必須調(diào)用 super 的原因與修復(fù) 派生類 extends 之后的子類自定義構(gòu)造函數(shù)時如果直接給 this 賦文檔/教程前端Redux action type 為什么必須用字符串Symbol 類型什么時候允許Redux action type 為什么必須用字符串Symbol 類型什么時候允許 在寫 Redux 應(yīng)用時 store.dispatch 傳入的 ac前端上一篇go-metrics高級技巧FanoutSink實現(xiàn)多后端指標(biāo)分發(fā)的完整方案下一篇Egg.js TypeScript實戰(zhàn)從JavaScript遷移到TypeScript完整指南創(chuàng)作聲明:本文部分內(nèi)容由AI輔助生成(AIGC),僅供參考