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