面向对象并非针对一种特定的语言,而是一种编程范式。可是每种语言在设计之初,都会强烈地支持某种编程范式,好比面向对象的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指向了全局做用域。
上面的面向对象主要解决了前文提到的两个痛点,可是也不是全部的业务场景都适合面向对象,当你的代码出现了一些坏味道(代码容易、代码分散不易处理),能够考虑下面向对象,毕竟适合的才是最好的