面向對象並非針對一種特定的語言,而是一種編程範式。可是每種語言在設計之初,都會強烈地支持某種編程範式,好比面向對象的Java,而Javascript並非強烈地支持面向對象。html
任何一名開發人員,在編寫具體的代碼的時候,不該該爲了套用某種編程範式,而去編寫代碼和改造代碼。任何編寫方式的目的是:前端
在個人平常工做中,最不想作的的就是兩點:es6
由於這兩種方式,會讓代碼冗餘,並且不易維護。爲何?編程
由於相同的代碼,具有相同的邏輯,也就是具有相同的業務邏輯場景,若是場景一旦改變,你將會改變兩處代碼。
ok,到這裏,咱們來說一個具體的業務場景。redux
場景1: 前端須要顯示工人的工做完成狀態,若是已經完成了,前端提供一個查看詳情的入口,若是沒有完成,提供工人去完成任務的入口。後端傳遞過來顯示工人完成狀態的字段:user_done_status:0,表明未完成,1表明已完成。前端須要實現這樣一個表格:
工人名字 | 完成狀態 | 操做 |
---|---|---|
小王 | 已完成 | 查看詳情 |
老王 | 未完成 | 去完成 |
// status.js // 1:須要一個狀態映射表,來實現第二列的功能 export const statusMap = new Map([ [0, '未完成'], [1, '已完成'] ]); // 2: 須要一個動做映射表,來實現第三列的功能 export const actionMap = new Map([ [0, '查看詳情'], [1, '去完成'] ]); // 3: 須要一個狀態判讀函數,來實現第三列的功能 function isUserDone(status) { return +status === 1; } const actionMap = new Map([ [status => isUserDone(status), userCanCheckResult], [status => !isUserDone(status), needUserToCompoleteWork] ]); function handleClick() { for (let [done, action] of actionMap) { if (done()) { actionMap(); return; } } }
至於第三個爲何這麼寫,能夠看一下這篇文章後端
上面的三段代碼單獨寫出來沒啥問題,看看下面的可能問題就出來,這至關於實現了三個函數,那麼須要在顯示在表格中就須要這樣寫:異步
import { statusMap, actionMap, getUserAction } from './status.js' .... .... // 第二列 return ( <span> { statusMap.get(status) } </span> ); // 第三列 return ( <span onClick={() => getUserAction(status)}> actionMap.get(status) </span> );
這樣的寫法,看起來沒啥問題,可是可讀性是不好的,主要體如今兩點:函數
可能有的人會說,這樣把上面的代碼單獨抽離出一個文件,也沒什麼問題,狀態也是比較集中的,嗯,這種說法也沒什麼問題,單獨提取一個文件,用做處理用的狀態,是一種常見的抽象方法。可是可能會遇到下面集中狀況,就會讓你很難受:post
業務場景變化,工人的任務狀態,添加了其餘限制,好比任務的時間限制,任務有未開始、進行中、已過時三種狀態,只有當在任務進行中的時候,才能夠展現用戶的狀態,不然就展現未開始或者已過時,總結起來,須要下面的幾種狀態:this
那麼顯然,你就須要修改代碼的邏輯,僅僅依靠一個statusMap就不能行了。固然這裏有人說了,那我把map編程一個函數:
const getUserStatus = (status, startTime, endTime) => { // ...do something }
這樣是否是就能夠了,嗯,說的也沒什麼問題,那你須要去修改以前寫的全部代碼,傳入不一樣的參數,就算一開始你用的不是map而是函數,那麼你的代碼也須要再傳入兩個多餘的參數,start_time和end_time。
最開始遇到這來那個問題的時候,我想的是怎麼樣可以把全部的處理集中到一塊兒,天然而然就想到了面向對象,將用戶的狀態做爲一個對象,對象具有特定的屬性和對應的操做行爲。
先睹爲快,咱們看一下,上面的代碼在面向對象的寫法,直接使用es6的class
import moment from 'moment'; class UserStatus { constructor(props) { const keys = [ user_done_status, start_time, end_time ] ; for (let key of keys) { this.[`_${key}] = (props || {})[key]; } } static StatusMap = new Map([ [0, '未完成'], [1, '已完成'] ]); static TimeMap = newMap([ [0, '未開始'], [1, '已過時'] ]); get userDoneStatus () { return this._user_done_status; } get isInWorkingTime() { const now = new Date(); return moment(now).isBetween(moment(this._start_time), moment(this._end_time)); } get isWorkStart() { const now = new Date(); return moment(now).isAfter(moment(now)); } get userStatus () { if (this.isInWorkingTime) { return UserStatus.StatusMap.get(this.userDoneStatus); } else { return UserStatus.TimeMap.get(+this.isWorkStart); } } ... ... // 省略其餘的了 }
那麼寫好了上面的類,咱們應該在其餘地方怎麼引用呢?
// 第一步:直接講後端傳過來的信息,構造一個新的對象 const userInfo = new UserStatus(info); // 第二步:直接調用對應的方法或者參數 return ( <span> { userInfo.userStatus } </span> );
之後不管業務場景如何改變這部分代碼都不須要從新改寫,只須要改寫對應的類的操做就能夠了。
這樣看了比較乾淨的是具體的view層代碼,就是簡單的html和對應的數據,沒有其餘操做。其實這就是如何消除代碼反作用的問題:將反作用隔離。當你把全部的反作用隔離以後,代碼看起來乾淨許多,你像redux-saga就是將對應的異步操做隔離出來。
ok,看了上面的類的寫法,咱們來看一下面向對象的寫法應該要怎麼寫:
特性 | 特色 | 舉例 |
---|---|---|
封裝 | 封裝就是對具體的屬性和實現細節進行隱藏,造成統一的的總體對外部提供對應的接口 | 上面的例子就是很好的解釋 |
繼承 | 繼承就是子類能夠繼承父類的屬性和行爲,也能夠重寫父類的行爲 | 好比工人有用戶狀態,老闆也有用戶狀態,他們均可以繼承UserStatus這一個基類 |
多態 | 同一個行爲在在不一樣的調用方式下,具有不一樣的行爲,依賴於抽象和重寫 | 好比工人和老闆都具有一個行爲那就是吃飯,工人吃的是饅頭,老闆吃的是海鮮,一樣是吃這個行爲,產生了不一樣的表現形式 |
在基本的面向對象中有幾個原則SOLID原則,可是這裏我不想詳細寫了,想說一下,我在封裝對象的時候會注重的幾個方面
class Base { constructor(props) { for (let key of props) { this[key] = props[key]; } } }
注意⚠️在js中必定小當心this的使用,假設有一個初始類:
初始類:
class Base { constructor(props) { this._a = props.a; } status() { return this._a; } }
避免下面的行爲:
// 方式1: let { status } = new Base({a: 678}); status() // 會報錯
而應該使用下面的寫法:
//方式2: let info = new Base({a: 678}); info.status(); //輸出正確
根本緣由就是this在做怪,第一種this指向了全局做用域。
上面的面向對象主要解決了前文提到的兩個痛點,可是也不是全部的業務場景都適合面向對象,當你的代碼出現了一些壞味道(代碼容易、代碼分散不易處理),能夠考慮下面向對象,畢竟適合的纔是最好的