<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>

          應(yīng)該在JavaScript中使用Class嗎?

          共 8842字,需瀏覽 18分鐘

           ·

          2020-09-01 10:17

          作者:FreewheelLee

          來源:https://zhuanlan.zhihu.com/p/158956514

          看似無處不在的OOP

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

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

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

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

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

          JavaScript中使用Class的坑

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

          看個例子:

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

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

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

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

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

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

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

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

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

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

          方案一

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

          **bind()**方法創(chuàng)建一個新的函數(shù),在bind()被調(diào)用時,這個新函數(shù)的this被指定為bind()的第一個參數(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`)
          }
          }

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

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

          方案二

          使用類屬性+箭頭函數(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`)
          }
          }
          }

          運行測試代碼,依然能成功輸出 Grey says hello

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

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

          class Student extends Person {
          talk = () => {
          super.talk(); // 報錯
          console.log("student talk hi");
          }
          }

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

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

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

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

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

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

          在 JavaScript 中,我們還有其他方案可以達到這個目的

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

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

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

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

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

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

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

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

          繼承

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

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

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

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

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

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

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

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

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

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

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

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

          個人體驗

          在我的工作負責(zé)的幾個項目中,其中一個Nodejs項目,我發(fā)現(xiàn)了大量的不必要的 class ,在constructor中充斥著大量的 bind 語句,而且這些 class 的方法之間并沒有太多關(guān)系,很多class也沒有內(nèi)部狀態(tài);更像是為了聲明這些函數(shù)是屬于同一個模塊而已 —— 也就是說根本不必要以 class 的形式組織代碼。

          于是,在日常任務(wù)完成之余,我就花了點時間,把這些class方法全部重構(gòu)成普通的 function,再利用 ES6 module 的形式重新組織這些代碼。現(xiàn)在這些代碼干凈、清晰了很多。

          (關(guān)于 Nodejs 的 module 以及如何在 Nodejs 中使用 ES6 module,歡迎閱讀我的另一篇文章 )

          FreewheelLee:[搬運] JavaScript 模塊化:CommonJS vs AMD vs ES6:


          補充:

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

          1. bind 以外的其他方案

          感謝 @賀師俊 大佬的提醒

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

          讀者們可以參考

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

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

          這個例子的確還不夠亮眼,那我再舉個更實際的例子吧

          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);

          對比

          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)了嗎?)

          3. 注意使用 class 的初衷

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

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

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

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

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

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

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

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

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

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

          FreewheelLee:[搬運] JavaScript 模塊化:CommonJS vs AMD vs ES6:

          4. 使用 class 的心智負擔(dān)

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

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

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

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

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

          giving clean needles to meth addicts

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

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

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

          可以參考這個回答

          brambles:現(xiàn)代瀏覽器生成一個 JS 函數(shù)的開銷多大?React hooks 的設(shè)計頻繁生成新函數(shù)對性能有影響嗎?www.zhihu.com

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

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

          6. @賀師俊 賀大提到另一個點

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

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

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


          》聲明:文章著作權(quán)歸作者所有,如有侵權(quán),請聯(lián)系小編刪除。

          瀏覽 52
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          <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>
                  a黄色免费 | 久操福利在线 | 高清毛片AAAAAAAAA片 | 影音先锋AV黄色免费电影! | 亚洲精品七区 |