<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          【JS】707- JavaScript 中使用 Class 的正確姿勢

          共 8931字,需瀏覽 18分鐘

           ·

          2020-09-06 04:10


          看似無處不在的OOP

          OOP 即 面向?qū)ο缶幊?(Object Oriented Programming)毫無疑問是軟件設(shè)計(jì)和發(fā)展中的一大進(jìn)步。事實(shí)上,一些編程語言如 Java 、C++ 就是基于 OOP 的核心概念 class 開發(fā)出來。

          在高校的 CS 相關(guān)專業(yè)中,無論教授什么編程語言,OOP的學(xué)習(xí)是絕對(duì)不會(huì)被落下的。

          同時(shí),OOP在業(yè)界中也的確被大量使用,尤其是的后端服務(wù)領(lǐng)域、桌面軟件、移動(dòng)APP開發(fā)等。

          因此,OOP看起來在軟件行業(yè)無處不在,在這種有點(diǎn)教條主義的氛圍下,很多程序員甚至以為 class 是編程固有的概念 —— 然而并不是。

          OOP 只是一套幫助開發(fā)者設(shè)計(jì)和編寫軟件的方法論,但并不代表它能解決所有領(lǐng)域的問題,也不是能在所有編程語言的任何場景下都適用。我們應(yīng)避免陷入這種教條主義。

          JavaScript中使用Class的坑

          ES6 之后,JavaScript 也引入了 class 關(guān)鍵字用于聲明一個(gè)類。但需要注意的是,這樣聲明出來的類其實(shí)在底層還是使用了 JavaScript 的函數(shù) 和 原型鏈 (來模擬類的行為)

          看個(gè)例子:

          class?Person?{
          ??constructor?(name)?{
          ????this.name?=?name
          ??}
          ??
          ??talk?()?{
          ????console.log(`${this.name}?says?hello`)
          ??}
          }

          上面的代碼在底層實(shí)現(xiàn)時(shí),非常接近于

          function?Person?(name)?{
          ??this.name?=?name
          }
          Person.prototype.talk?=?function?()?{
          ??console.log(`${this.name}?says?hello`)
          }

          這邊可以注意到 talk 其實(shí)并不是一個(gè)Person類內(nèi)部封裝的方法,而只是一個(gè)常規(guī)的JavaScript函數(shù),賦值到了Person的原型上而已。因此,「talk 函數(shù)里的 this 對(duì)應(yīng)的是調(diào)用時(shí)的上下文而不是定義時(shí)的上下文」,這點(diǎn)跟 Java 和 C++ 的差別很大。

          這種差異最明顯的影響是在別的對(duì)象試圖調(diào)用這個(gè)對(duì)象的talk時(shí)

          const?Grey?=?new?Person('Grey')
          const?mockDomButton?=?{}?//?模擬一個(gè)DOM上的按鈕對(duì)象
          mockDomButton.onClick?=?Grey.talk;?//?綁定點(diǎn)擊事件
          mockDomButton.onClick()?//?輸出的結(jié)果是?undefined?says?hello

          上面這段模擬代碼輸出的結(jié)果并不是我們想要的。原因是 onClick 被調(diào)用時(shí),其實(shí)是 talk 函數(shù)在執(zhí)行,且talk 函數(shù)的this 指向的是 mockDomButton 而不是 GreymockDomButton 并沒有 name 屬性于是 輸出了 undefined says hello

          這種“特殊”的表現(xiàn)讓很多 JavaScript 新手感到頭疼,尤其是那些從 Java 或者 C++ 背景過來的新手前端程序員。

          解決這個(gè)問題的辦法當(dāng)然是有的,先介紹兩個(gè)仍然使用 class 的方案

          「方案一」

          使用函數(shù)的 bind 方法

          ?

          **bind()**方法創(chuàng)建一個(gè)新的函數(shù),在bind()被調(diào)用時(shí),這個(gè)新函數(shù)的this被指定為bind()的第一個(gè)參數(shù)

          ?

          修改 Person.js 文件如下

          class?Person?{
          ??constructor?(name)?{
          ????this.name?=?name
          ????this.talk?=?this.talk.bind(this);?//?在構(gòu)造器里顯式調(diào)用?bind?函數(shù)綁定?this
          ??}

          ??talk?()?{
          ????console.log(`${this.name}?says?hello`)
          ??}
          }

          再次運(yùn)行上面的測試代碼,這次的輸出就是正確的了 —— Grey says hello

          這種方案的缺點(diǎn)就是需要繁瑣地寫這種 bind 方法調(diào)用語句,當(dāng)這個(gè)類的方法很多時(shí),會(huì)顯得構(gòu)造器非常臃腫,降低可讀性和編碼效率如

          img

          「方案二」

          使用類屬性+箭頭函數(shù)的方式來定義方法

          class?Person?{
          ??constructor(name)?{
          ????this.name?=?name
          ??}

          ??talk?=?()?=>?{
          ????console.log(`${this.name}?says?hello`)
          ??}
          }

          這種語法是 ES2017 才引入的,它等效于

          class?Person?{
          ??constructor(name)?{
          ????this.name?=?name
          ????this.talk?=?()?=>?{
          ??????console.log(`${this.name}?says?hello`)
          ????}
          ??}
          }

          運(yùn)行測試代碼,依然能成功輸出 Grey says hello

          但是,這種方案也有缺點(diǎn) —— 由于它等效于函數(shù)定義放在了構(gòu)造器內(nèi),所以

          一、這個(gè)方法不在原型鏈上,即 Person.prototype.talk 的值是undefined ,所以這個(gè)類的子類并不能使用 super.talk() 調(diào)用到父類這個(gè)方法,所以下面這段代碼會(huì)報(bào)錯(cuò)

          class?Student?extends?Person?{
          ??talk?=?()?=>?{
          ????super.talk();?//?報(bào)錯(cuò)
          ????console.log("student?talk?hi");
          ??}
          }

          const?student?=?new?Student('Tom');
          student.talk();

          二、每次創(chuàng)建一個(gè) Person 實(shí)例都會(huì)創(chuàng)建一個(gè) talk 函數(shù),造成性能浪費(fèi) (僅僅是用來與方案一對(duì)比)

          const?Grey?=?new?Person('Grey')
          const?Tom?=?new?Person('Tom')
          console.log(Grey.talk?===?Tom.talk);?//??輸出?false

          在 JavaScript 中使用類居然有上面這么多坑,那何不試試其他方案?

          首先,我們回到源頭想想什么是類,我們想利用類達(dá)到什么目的:

          大多數(shù)時(shí)候,我們定義的類 其實(shí)是 創(chuàng)建對(duì)象的藍(lán)圖(模板) —— 我們先規(guī)劃好一個(gè)類的模樣,之后通過 new 的方式創(chuàng)建出許許多多的對(duì)象,每個(gè)對(duì)象都符合我們想要的格式(即屬性,方法)

          在 JavaScript 中,我們還有其他方案可以達(dá)到這個(gè)目的

          工廠函數(shù)(factory functions)

          const?PersonFactory?=?(name)?=>?{
          ??return?{
          ????talk:?()?=>?{
          ??????console.log(`${name}?says?Hello`)
          ????}
          ??}
          }

          PersonFactory 是個(gè)簡單的工廠函數(shù),它返回一個(gè)對(duì)象,這個(gè)對(duì)象擁有一個(gè) talk 方法

          (p.s. 我更新了一下代碼,看起來可讀性更高一點(diǎn),想看原版代碼的可以查看歷史記錄

          const?Grey?=?PersonFactory('Grey');?//?使用工廠函數(shù)生成對(duì)象
          const?mockDomButton?=?{}?//?模擬一個(gè)DOM上的按鈕對(duì)象
          mockDomButton.onClick?=?Grey.talk;?//?綁定點(diǎn)擊事件
          mockDomButton.onClick()?//?輸出的結(jié)果是?Grey?says?Hello

          由于JavaScript的「閉包」特性,name已經(jīng)被封裝在了函數(shù)里,所以上面的測試代碼可以正常運(yùn)作。而且更贊的是,這個(gè)方案中,name甚至自動(dòng)成為了「私有的變量」,不怕被更改(上面的那些 class 方案里 name 都可以被公共訪問的)

          而且相比之下,工廠函數(shù)的代碼更簡潔易讀,也不需要考慮 this 的繁瑣問題。

          因此,「如果只是為了給對(duì)象創(chuàng)建繪制藍(lán)圖(模板),工廠函數(shù)是比類更合適的方案」。

          繼承

          類的另一個(gè)特征是繼承機(jī)制,子類可以繼承(分享)來自父類的屬性和方法。

          如果僅僅是共享屬性和方法,使用組合(composition)也可以很容易實(shí)現(xiàn)

          const?Workable?=?{
          ??inOffice:?true
          }
          const?WorkablePersonFactory?=?(name)?=>?(
          ??Object.assign(
          ????{},
          ????Workable,
          ????PersonFactory(name)
          ??)
          )
          //?或者
          const?WorkablePersonFactory?=?(name)?=>?(
          ????{
          ?????...?Workable,
          ?????...PersonFactory(name),
          ????}
          )

          上面的代碼意圖十分明顯,可讀性很高,這也是組合模式的一個(gè)優(yōu)點(diǎn)。

          當(dāng)然,對(duì)于某些更復(fù)雜的類使用場景,工廠函數(shù)并不能替代類。

          關(guān)注代碼表達(dá)性而不是死守教條主義

          在 JavaScript 的現(xiàn)實(shí)場景中,尤其是前端代碼,我們很少真正用到類繼承,大多數(shù)時(shí)候,工廠函數(shù)就能完成我們的目標(biāo)。

          以React為例,官方這幾年推崇 Hooks 的意圖也很明顯 —— 擺脫JavaScript class 帶來的復(fù)雜性,擁抱函數(shù)式風(fēng)格。

          由于 JavaScript 實(shí)現(xiàn)的特殊性,在 JavaScript 應(yīng)用中使用 class 對(duì)于一些程序員來說有許多坑,于此同時(shí),大多數(shù)場景下其他替代方案如 工廠函數(shù) 可能更契合JavaScript的特性,反而帶來更好的效果。

          當(dāng)然,「并不是一桿子打死 JavaScript 的 class,在一些特別適合 OOP 的場景中,依然鼓勵(lì)使用 class」

          總之,不要被教條主義所束縛,牢記編寫程序最重要的兩點(diǎn)是:

          1. 真正將需求轉(zhuǎn)化成了代碼
          2. 寫出可讀的,容易維護(hù)的,方便理解的代碼

          沒想到這篇文章有這么高的閱讀量,以及部分爭議。統(tǒng)一回復(fù)一下吧。

          「本文的討論的場景主要是基于業(yè)務(wù)開發(fā)的上下文,不包括底層庫、工具庫開發(fā)等場景。」

          1. bind 以外的其他方案

          感謝

          @賀師俊

          大佬的提醒

          ?

          class fields或者autobind decorator都有很多問題,而且這兩者還不是最終標(biāo)準(zhǔn),建議不要用

          ?

          讀者們可以參考

          關(guān)于 工廠函數(shù) 的舉例

          首先這個(gè)例子主要是針對(duì)這種場景 ——在 JavaScript 給創(chuàng)建某類對(duì)象定制一個(gè)標(biāo)準(zhǔn),以便可以用這個(gè) 「模板」 創(chuàng)建許多對(duì)象

          這個(gè)例子的確還不夠亮眼,那我再舉個(gè)更實(shí)際的例子吧

          function?httpClientFactory(baseUrl)?{
          ??return?{
          ????baseUrl:?baseUrl,
          ????listUsers:?()?=>?{
          ??????return?axios.get(`${baseUrl}/users`)
          ????},
          ????getUser:?(id)?=>?{
          ??????return?axios.get(`${baseUrl}/users/${id}`)
          ????},
          ????createUser:?(user)?=>?{
          ??????return?axios.post(`${baseUrl}/users`,?user);
          ????},
          ????listBooks:?()?=>?{
          ??????return?axios.get(`${baseUrl}/books`)
          ????},
          ????getBook:?(bookName)?=>?{
          ??????return?axios.get(`${baseUrl}/books/${bookName}`)
          ????},
          ????createBook:?(book)?=>?{
          ??????return?axios.post(`${baseUrl}/books`,?book)
          ????}
          ??}
          }

          const?httpClient?=?httpClientFactory("https://your-endpoints/api");
          httpClient.getUser("123");
          httpClient.getBook("JavaScript?Is?Interesting");
          console.log("The?httpClient's?baseUrl?is?"?+?httpClient.baseUrl);

          對(duì)比

          class?HttpClient?{
          ??constructor(baseUrl)?{
          ????this.baseUrl?=?baseUrl;
          ????this.listUsers?=?this.listUsers.bind(this);
          ????this.getUser?=?this.getUser.bind(this);
          ????this.createUser?=?this.createUser.bind(this);
          ????this.listBooks?=?this.listBooks.bind(this);
          ????this.getBook?=?this.listUsers.bind(this);
          ????this.createBook?=?this.createBook.bind(this);
          ??}

          ??listUsers()?{
          ????return?axios.get(`${this.baseUrl}/users`)
          ??}

          ??getUser(id)?{
          ????return?axios.get(`${this.baseUrl}/users/${id}`)
          ??}

          ??createUser(user)?{
          ????return?axios.post(`${this.baseUrl}/users`,?user);
          ??}

          ??listBooks()?{
          ????return?axios.get(`${this.baseUrl}/books`)
          ??}

          ??getBook(bookName)?{
          ????return?axios.get(`${this.baseUrl}/books/${bookName}`)
          ??}

          ??createBook(book)?{
          ????return?axios.post(`${this.baseUrl}/books`,?book)
          ??}
          }

          const?httpClient?=?new?HttpClient("https://your-endpoints/api");
          httpClient.getUser("123");
          httpClient.getBook("JavaScript?Is?Interesting");
          console.log("The?httpClient's?baseUrl?is?"?+?httpClient.baseUrl);

          感受一下代碼的整潔程度

          (彩蛋:bind 語句復(fù)制粘貼導(dǎo)致的bug你們發(fā)現(xiàn)了嗎?)

          注意使用 class 的初衷

          太多開發(fā)者一上來就寫個(gè)class的原因通常是因?yàn)?他/她 是從OOP背景過來的 —— 在Java,你不能光禿禿地定義一個(gè)常量,一個(gè)函數(shù)或者一個(gè)表達(dá)式,你得先有個(gè)類,然后在類里定義一個(gè)靜態(tài)不可變的屬性 (public static final 三連) 才能產(chǎn)生一個(gè)常量,類似的,也只能在類里定義一個(gè)(靜態(tài)或者非靜態(tài))的方法才能讓函數(shù)有容身之地 (為了防杠,我謹(jǐn)慎加一條 —— Java 8 的 functional interface 開始可以讓函數(shù)單獨(dú)出來走兩步了,但前提還是要有interface)

          如果你想好好寫 native JavaScript,那么你通常不需要一個(gè)類

          //?xxx.js
          import?_?from?'lodash';

          export?const?BOOK_NAME_PREFIX?=?"JS_";?//?定義常量
          export?const?DEFAULT_USER_AGE?=?18;

          export?const?convertVarToObject?=?function?(v)?{?//?定義一個(gè)工具方法,將傳入的值包裝返回一個(gè)對(duì)象
          ??//?...
          }

          const?privateSecret?=?"zhimakaimen";?//?不export的常量自然變成模塊私有的

          function?privateFunc(){??//?同樣可以定義模塊私有的函數(shù)
          ???//?...?
          }

          export?default?{??//?可以export出自定義的對(duì)象(包含自定義的屬性)
          ???render:?xxx,??
          ???property:?yyy,
          }

          直接在 js module 里定義常量、函數(shù),然后 export 出來給其他模塊用,這么簡單直接不香嗎?(js module 里也可以定義私有的變量、常量、函數(shù)等)

          再次推薦閱讀 這篇文章,好好理解 js 模塊,別再像 Java 那樣只用 class 來組織所有代碼了。

          JavaScript 模塊化:CommonJS vs AMD vs ES6:https://zhuanlan.zhihu.com/p/158683510

          使用 class 的心智負(fù)擔(dān)

          業(yè)務(wù)代碼中,現(xiàn)在大家寫 JavaScript class 相信已經(jīng)不會(huì)再直接訪問 prototype 了,而是使用 class 關(guān)鍵字 —— 而 class 關(guān)鍵字的底層實(shí)現(xiàn)仍然是 prototype,仍然要考慮 this 的復(fù)雜性,在復(fù)雜的繼承場景中甚至仍然得理解 prototype chaining

          也就是說,一個(gè)新手接觸/維護(hù)一個(gè)由大量類構(gòu)成的項(xiàng)目時(shí),他要么趕緊精通理解JavaScript class,要么就很可能掉進(jìn)坑里。

          我在個(gè)人體驗(yàn)里談到的那個(gè)Nodejs項(xiàng)目,實(shí)習(xí)生新增一個(gè)方法后忘記加bind語句,然后程序一直報(bào)錯(cuò) ReferenceError: XXX is not defined, 他一頭霧水 —— ”明明方法定義就在那兒啊!“

          當(dāng)然這是因?yàn)閷?shí)習(xí)生的基礎(chǔ)問題,他需要更多學(xué)習(xí)歷練,但話說回來**這樣的心智負(fù)擔(dān)真的有必要嗎?為什么不讓程序更簡單明了一點(diǎn)?**僅僅是為了讓代碼看起來更 OOP 嗎?

          這個(gè)油管視頻 https://www.youtube.com/watch?v=Tllw4EPhLiQ (有條件的讀者可以看看) 里說 「在 JavaScript添加 class 關(guān)鍵字」 就好像

          ?

          giving clean needles to meth addicts

          ?

          給(xi du的)癮君子送來一些干凈的針頭 (太犀利了?。ㄓ锌鋸埑煞?狗頭護(hù)體)

          簡單來說,JavaScript 并不擅長玩 OOP class 這一套,它有自己非常擅長且自然而然的風(fēng)格(函數(shù)式),如果你想好好學(xué) JavaScript 且正宗地用好 JavaScript ,我個(gè)人十分建議,把你花在 JavaScript OOP上的時(shí)間用來先搞清楚 JavaScript function 和 閉包 (React 開發(fā)者學(xué)好 Hooks)—— 然后再去學(xué) class、prototype 等知識(shí)

          「牢記JavaScript的一個(gè)特性 —— Functions are first-class in JavaScript 函數(shù)是一等公民」

          工廠函數(shù)會(huì)每次都重復(fù)生成函數(shù)(影響性能)嗎?

          可以參考這個(gè)回答

          https://www.zhihu.com/answer/943385371

          另外,可以簡單回想一下,在我們?nèi)粘I(yè)務(wù)開發(fā)中,真的有需要?jiǎng)?chuàng)建那么多類對(duì)象嗎?

          你寫的類里被 new 過幾次?真的每次 new 都有必要嗎?如果沒有,往上看第 3 點(diǎn)。

          @賀師俊

          賀大提到另一個(gè)點(diǎn)

          ?

          class具有更高的聲明性和靜態(tài)可分析性,也跟platform api更為一致,同時(shí)在現(xiàn)代引擎里也有更好的優(yōu)化

          ?

          感謝賀大的指出,底層庫的開發(fā)我本人經(jīng)歷不多,目前接觸更多是還是業(yè)務(wù)代碼為主。

          至于引擎的代碼優(yōu)化,我持保留意見,之前在研究React Hooks的時(shí)候,不記得在哪看到過React的官方開發(fā)者認(rèn)為在未來 Functional Component 的優(yōu)化有比 Class Component 更好的趨勢(原句和原文我暫時(shí)找不到了,找到了再補(bǔ)充回來,有讀者看到過也可以評(píng)論給我,謝謝) —— 更新:找到了 https://zh-hans.reactjs.org/docs/hooks-intro.html#classes-confuse-both-people-and-machines

          img

          后記

          挺意外這篇文章有這么大的關(guān)注度,多謝大家的支持和討論。

          其實(shí)我個(gè)人還是有點(diǎn)耿耿于懷的,雖然文章整體表達(dá)了我的觀點(diǎn),但感覺并沒有完全把 JavaScript class 的所有坑介紹清楚(僅提了比較常見的 bind 問題),其實(shí)還有 prototype 的機(jī)制差異、prototype chain 等問題,但是限于篇幅就沒寫出來。

          接下來我會(huì)繼續(xù)寫一篇后續(xù)的相關(guān)的文章,接著討論 JavaScript 和 OOP 碰撞的另一簇火花 —— 原來不使用 class ,JavaScript 依然能借鑒前人OOP的最佳實(shí)踐和經(jīng)驗(yàn)!



          1. JavaScript 重溫系列(22篇全)
          2. ECMAScript 重溫系列(10篇全)
          3. JavaScript設(shè)計(jì)模式 重溫系列(9篇全)
          4.?正則 / 框架 / 算法等 重溫系列(16篇全)
          5.?Webpack4 入門(上)||?Webpack4 入門(下)
          6.?MobX 入門(上)?||??MobX 入門(下)
          7.?70+篇原創(chuàng)系列匯總

          回復(fù)“加群”與大佬們一起交流學(xué)習(xí)~

          點(diǎn)擊“閱讀原文”查看70+篇原創(chuàng)文章

          瀏覽 54
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評(píng)論
          圖片
          表情
          推薦
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  大香蕉网婷婷 | 日韩美女射 | 操女人网| 日韩有码一区 | 视频一区二区三区四 |