JS是否有必要使用面向对象、设计模式
在一次面试过程中,一位已经有5年工作经验的前端,在回答面试问题时这样说到。
问:你能说说JS的面向对象和设计模式吗?
回答说:这些内容主要是后端的Java,C#这种高级语言才会用到的,前端一般我们没有用到。
对于这样的回答,不禁让我有点无话可说,JS中是否有必要使用面向对象以及设计模式呢?我列举了以下几个场景:
数据接口请求
一般的,在请求接口方面,我们一般会使用一些第三方库,比如axios
。然后在逻辑代码部分,比如在组件中直接使用axios
进行请求,例如:
let methods = {
/**
* 获得分类信息
*/
async getBarData () {
try {
let res = await axios.get(url,params)
}catch (e) {
console.error('something error' ,e)
}
},
}
这样的做法在功能上讲没什么问题,但在新增一些其他动作后,这样的做法就变得非常难以管理。比如,需要在请求中加入一些关联请求,需要获取一个商品页的列表,查询参数包含,分页参数(当前页,查询数),分类Id,搜索内容,排序方式,筛选项。执行该去请求时,发现分类Id也需要另外一个接口去获取。于是代码成了:
let params2 = {
sort:-1,
search:'',
filter:'',
page:{
start:1,
number:10
}
}
let methods = {
/**
* 获得商品列表
*/
async getGoodsData () {
try {
let {id:typeId} = await axios.get(url.goodsType,params1) // 获取所有分类Id
let res = await axios.get(url.goods,{...params2,typeId}) // 获取商品
}catch (e) {
console.error('something error' ,e)
}
},
}
上面的代码中,我们简单的实现了获取一个接口的值然后请求另外一个接口。那么如果当前的搜索内容、或者分页数据修改了,还需要重新获取新的商品数据,此时getGoodsData
还需要执行一遍,而获取分类的请求又需要请求一遍,所以需要改动代码为:
let params= {
sort:-1,
search:'',
filter:'',
page:{
start:1,
number:10
}
}
let methods = {
/**
* 获得分类信息
*/
async getTypeData () {
try {
let {ids} = await axios.get(url.goodsType,params1) // 获取所有分类Id
this.typeIdNow = ids[0]
}catch (e) {
console.error('something error' ,e)
throw e
}
},
/**
* 获得商品列表
*/
async getGoodsData () {
try {
(this.typeIdNow === undefined) && await getTypeData()
let typeId = this.typeIdNow
let res = await axios.get(url.goods,{...params,typeId}) // 获取商品
}catch (e) {
console.error('something error' ,e)
}
},
}
当params
的任意数据改变后会请求getGoodsData
,这样暂时我们已经实现了一个商品请求的逻辑,并且支持数据暂存。
紧接着问题又来了,切换类别时会要求获取新的筛选列表(不同的分类下筛选列表是不同的)。
切换类别后,会要求重置params
,因为之前的搜索值,分页值在切换类别后不能继续使用。
字段组装,比如筛选字段的filter
,一般的后台可能会用一些特殊的分隔符比如(|)来做多个筛选项的分割,此时我们又需要处理以下的代码:
return this.types.map(val=>val.id).join('|')
节流优化,用户输入Value时,需要做防抖函数的优化,防止一直请求接口。
恩,终于,当一大堆问题都解决后,需求来了,能不能在其他组件使用这些数据?? 哇特?
回顾一下,我们要做的就是一个内容,getGoodsData
为什么会同时出现这么多代码呢?一个商品列表的组件会需要这么多组装数据的代码吗?
面向对象优化
面对这种让人抓耳挠腮,看着头晕的代码难道就没有更优雅的实现方式吗?面向对象了解一下,数据模型了解一下!
我们可以将Goods
这一中数据类型抽象成为一种资源对象,在Model中专门处理Goods
获取时所需要的数据组装等工作。
import { API, axios } from '../api'
/**
* 商品列表数据
*/
class Goods {
private params: Object = {};
private initParamsData: Object = {
sort:-1,
search:'',
filter:'',
page:{
start:1,
number:10
}
};
constructor() {
this.initParams()
}
/**
* 初始化所有请求参数
*/
public initParams() {
this.params= JSON.parse(JSON.stringify(this.initParamsData)) // 深拷贝
}
/**
* 设置请求参数
* @param key
* @param val
*/
public setParams(key, val) {
this.params[key] = val
}
/**
* 获取商品请求
*/
public async get(params = {}) {
let {id:typeId} = await Type.get() // 在另外一个Type类中获取并做缓存处理
params = { ...this.params, ...params ,typeId}
let res = await axios.get(API.GOODS_LIST, { params })
return res
}
public async save() {
}
}
export default new Goods()
然后就可以在组件中优雅的进行使用,Goods
的数据模型中已经可以自行处理依赖请求,缓存数据,参数组装等功能,在另外组件使用中也同样可以使用相同的数据和缓存,代码如下:
let methods = {
/**
* 获得商品列表
*/
async getGoodsData () {
try {
let res = await Goods.get() // 获取商品
}catch (e) {
console.error('something error' ,e)
}
},
/**
* 设置请求参数
* @param key
* @param val
*/
setParams(key,val){
// 设置请求参数,对于部分需要特殊组装的字段可以在类中单独分装方法处理
Goods.setParams(key,val)
}
}
状态管理
一般的,在处理多组件数据通信时,会使用Redux/Mobx/Vuex
这类Flux
模式的状态管理库来处理。对于Vuex
来讲,一般会在实例化一个Vuex
,设置其state
对象以及对应的mutations
,然后将其挂载到Vue的原型链中,就可以方便的完成响应式的状态管理。
实际上,当项目中的不同路由层级,不同组件,不同生命周期的状态都混在一个state
中管理是很混乱,很不明智的做法。当然Vuex中也提供了Modoles
的用法,让我们可以通过不同的模块化来管理不同状态,从某种程度上来讲这也是一种面向对象的做法。
const moduleA = {
state: { ... },
mutations: { ... },
actions: { ... },
getters: { ... }
}
const moduleB = {
state: { ... },
mutations: { ... },
actions: { ... }
}
const store = new Vuex.Store({
modules: {
a: moduleA,
b: moduleB
}
})
store.state.a // -> `moduleA`'s state
store.state.b // -> `moduleB`'s state
但相对于Mobx
的Class式用法,这种Vuex
中Module
的用法还是显得有些麻烦,让我们来看看Mobx
的Class定义状态管理是如何处理的:
import {computed, observable} from "mobx"
class StoreData {
@observable isSideBarShow = true // 是否显示侧边栏
@observable routeNow: any = {}
public setSideBarShow( val ) {
this.isSideBarShow = val
}
public setRoute( val ) {
this.routeNow = val
}
}
export default new StoreData()
相比与Mobx
的这种状态管理,引入了Es7中的装饰器,在编写代码中如果有使用其中所需要的状态值,可以直接方便的引入StoreData
,然后直接使用即可。
从定义、使用、修改各种步骤都更容易理解和使用。
很遗憾的是,Mobx
目前还没有完善的方案在Vue中使用。
Vuex
社区中,也有很多开发者有着同样的感受,所以出现了类似Vuex-class
这样的库,来使用Class
模式编写状态管理。也希望Vuex
官方能对面向对象编程有更好的支持!
组件继承
组件可以继承吗?比如我写了一个投票页面,用户是可以操作进行投票的,但是当他投票结束后,这个页面就不能再进行交互操作了,用户只能查看已经投票的结果。
正常的,我们可能会使用一个状态值来判断是否开放编辑功能。但是,当投票活动再加入:开始前,正在投票,投票结束等状态后。需要处理的就是:开始前已经投票的状态,开始前未投票的状态,正在投票已经投票的状态....这样6种状态。虽然从其他维度可以解决这样的问题,但状态再多复杂度就会再增加一层。如果还这样写,那么,恭喜你,传说中的面条代码就产生了!
所以我们最好还是用两个组件来做这件事情,一个组件用于提交,一个组件用于查看。在完成提交组件后,再实现查看组件时,会发现非常多的代码都是重复的 ,就比如投票数据获取,样式,模板,状态管理。
此时,组件继承的需求就变得愈发强烈,实际上,组件继承是可以实现的。拿Vue来说,在Vue2.5后Vue推出了vue-class-component。看看以下组件Test1。
<!--component 1-->
<template>
<h1></h1>
</template>
<script>
import Vue from 'vue'
import Component from 'vue-class-component'
@Component()
export default class Test1 extends Vue {
// initial data
msg = 123
// use prop values for initial data
helloMsg = 'Hello, ' + this.propMessage
// lifecycle hook
mounted () {
this.greet()
}
// computed
get computedMsg () {
return 'computed ' + this.msg
}
// method
greet () {
alert('greeting: ' + this.msg)
}
}
</script>
<style scoped>
</style>
继承组件Test2,在继承Test1后就可以使用Test1中定义的变量、样式,对于模板,实际上Vue在template中的内容最终也会被转移为render
函数中返回的模板值,如果你的JSX了解的话,你可以把它理解为JSX,只不过Vue把它换了一个位置,如果你想在Vue中使用JSX同样是可以实现的,参考文档。那么Test1就可以写成:
<!--component 2-->
<script>
import Test1 from './Test1'
import Component from 'vue-class-component'
@Component()
export default class Test2 extends Test1 {
mounted() {
super.mounted()
}
}
</script>
JavaScript中是如何实现面向对象的
说了这么多,那么在JavaScript中是如何实现Class
的呢?在ES5的标准中,是并没有Class
关键字的。
在JavaScript中的所有数据跟对象都是Object.prototype对象。我们在JavaScript遇到的每个对象,实际上都是从Object.prototype对象克隆而来的,Object.prototype就是他们的原型。
在JavaScript中执行的new Object()
,在内部引擎中实际上是从Object.prototype上面克隆一个对象出来,我们最终得到的才是这个对象。
function Person(gender){
this.gender = gender
}
Person.prototype.getGender=function(){
return this.gender
}
var me = new Person(1)
console.log(me.getGender())
在JavaScript中没有类的概念,但上面这段代码中不是明明调用了new Person()
吗?
在这段代码中,Person
并不是一个类,而是函数构造器。当使用new
来调用函数时,实际上是在克隆Object.prototype
对象,然后再才开始运行函数。
所以当我们得到me
时,内部已经完成了对Person.protorype
的克隆,当请求me.getGender
时,JavaScript完成了以下几步操作:
- 尝试查找
me
对象中是否有getGender
属性 - 没找到
genGender
,把该请求委托给Person
的构造器原型,原型会被记录在__proto__
中。 - 在原型链中找到了
getGender
属性,并返回它的值
多态与继承
多态的实际含义是:同一操作作用于不同对象上面,可以产生不同的解释和不同的执行结果。多态分为编译期多态、运行期多态。
举个例子:两个人打招呼,不同的人见面打招呼的方式不一样。比如好基友见面,会说:Hey,老哥~。陌生人见面会说:你好,幸会。Github上提问会说:Hello,nice to ...。韩国人见面打招呼会用韩语,日本人见面打招呼用日语等等。
用一段JavaScript代码来实现英国人和中国人打招呼:
var Chinese = function(){}
var British = function(){}
var sayHello = function(man) {
man instanceof Chinese && console.log('你好')
man instanceof British && console.log('Hello')
}
sayHello(new Chinese()) // 编译期多态
sayHello(new British())
这段代码确实实现了"多态性",当在发出sayHello
的命令后,不同的人会执行不同的打招呼方式,但却不是理想化的。试想如果后来新增了一个俄罗斯人,就必须要修改sayHello
函数,才能实现俄罗斯人打招呼。那么后面再加入对不同人打招呼,再增加其他国家的人,sayHello
函数将会变得非常庞大和难以维护。
从源头来看sayHello
这个动作中要输出什么的逻辑是由不同类型的人定义的,所以应该将sayHello
封装起来,作为不同类型的人sayHello
的一种方法。这就属于一种面向对象,代码变成了一种可扩展,可生长的代码。修改,并加入俄罗斯人的代码:
var Chinese = function(){}
Chinese.prototype.sayHello = function(){
console.log('你好')
}
var British = function(){}
British.prototype.sayHello = function(){
console.log('Hello')
}
var Russian = function(){}
Russian.prototype.sayHello = function(){
console.log('#&(*$(K')
}
var sayHello = function(man){
man.sayHello() // 运行期多态
}
sayHello(new Chinese()) // 编译期多态
sayHello(new British())
sayHello(new Russian())
在实现多态的同时,JavaScript中同样可以使用继承来实现类的多样性。比如我和MilkGao都是中国人,我们一般遇到其他人会说"你好"来打招呼,但是我们俩见面后因为一些其他的原因,打招呼的方式会不一样。我见到他会说:Hey,老哥。他见到我会说:哇,帅哥!
分析这段逻辑,两个人都是在跟特定的人打招呼(做同样的动作),两个人都是中国人,遇到陌生人都会说"你好",来打招呼。两个人不同的地方是,相互见面后打招呼的内容不同。所以可以都继承中国人来处理相同的打招呼逻辑,又有各自不同的遇到朋友的打招呼方法。
var Chinese = function(){}
Chinese.prototype.sayHello = function() {
console.log('你好')
}
var YeeWang = function() {
Chinese.call(this)
}
YeeWang.prototype = Object.create(Chinese.prototype)
YeeWang.prototype.constructor = YeeWang
YeeWang.prototype.sayHelloTo = function(man){
if(man instanceof MilkGao) console.log("Hey,老哥!")
else this.sayHello()
}
var MilkGao = function() {
Chinese.call(this)
}
MilkGao.prototype = Object.create(Chinese.prototype)
MilkGao.prototype.constructor = MilkGao
MilkGao.prototype.sayHelloTo = function(man) {
if(man instanceof YeeWang) console.log("哇,帅哥!")
else this.sayHello()
}
var twoPersonSayHello = function(man1,man2){
man1.sayHelloTo(man2) // 运行期多态
}
twoPersonSayHello(new YeeWang(),new MilkGao()) // 编译期多态
twoPersonSayHello(new MilkGao(),new YeeWang())
TypeScript
既然说JavaScript的面向对象,就不能不提TypeScript。
关于TypeScript的文档我就不具体介绍了,如果官网有详细的TypeScript的使用、规范说明。我列出了几点关于TypeScript相对于JavaScript的优势点和注意事项。
-
首先TypeScript编码过程中要求对变量进行类型定义,比如在项目中一旦定义一个变量的类型后,如果赋值类型不同,在编译器中就会直接报错,这或许在你看来比起JavaScript这显得非常麻烦,但对于长期受益来讲这会显得非常有用。
-
自动提示,使用TypeScript定义好
Class
后,在使用过程中,都会有对这个类的自动提示,在编码过程中一路回车,体验真的不要太好!相比于之前使用JS时借助IDE一些插件实现的关键字自动检索,TypeScript的提示速度更快更准确!
-
参数提示,在使用TypeScript编码时,如果遇到陌生的方法,可以直接快速追溯到该方法的定义,迅速查找参数类型。比如在使用
lodash
中的方法函数,就可以快速查到findIndex中所需要到参数类型,以及返回类型。
定义文件(.d.ts),使用TypeScript一定要注意的一点是,如果引入非TypeScript写的库。发现import报错,那么很有可能该库没有更新配置TypeScript,目前大多数用到的库都已经有对TypeScript的支持包括
Vue
,React
,Lodash
等等,但还是有一些库官方并没有更新.d.ts
的类型定义文件,对于这类文件TypeScript
另外做了一个开源项目,专门整理各大库的定义文件。比如three
这个库,如果要使用TypeScript,只需要运行npm i @types/three -D
就可以匹配找到该库的类型定义文件啦。
模块化
一套优秀的系统源码,是文件多、还是文件大?
对于上面这个问题答案是肯定的,一套优秀的系统源码应该是尽可能将逻辑颗粒度细化,尽可能的抽象和模块化可以使业务代码变得相对较少。
究竟什么是模块化?其实在Vue/React中的组件,就属于模块化,每个组件都被抽象成为一个module暴露出来,在其他组件中被使用,并被框架按照自己的组件处理方式制作成最终业务效果。
以下代码都是在对外暴露一个模块。
export default { } // ES6
module.exports = {}
静态加载与动态加载
- 静态加载:在编译阶段进行,把所有需要的依赖打包到一个文件中
- 动态加载:在运行时加载依赖
AMD标准是动态加载的代表,而CommonJS是静态加载的代表。
AMD的目的是用在浏览器上,所以是异步加载的。
而NodeJS是运行在服务器上的,同步加载的方式显然更容易被人接收,所以使用了CommonJS。
import Gallery from '@/views/Gallery' // 静态加载
const Gallery = () => import('@/views/Gallery') // 动态加载
为什么要使用模块化?
为什么要使用组件呢?
在很早以前(我还在做PHP的时候)和朋友在谈起Laravel框架时说到:恩,我觉得这个框架很强大,很多代码都是一个方法里面嵌套了很多其他方法,代码阅读起来非常舒服。朋友:我最讨厌这样的写法,一层嵌一层都不知道他在干什么。我:...
为什么要使用模块化?为了尽可能的少写代码。
使用模块化可以让我们在编写代码时,会"少写"很多代码。
我们在实现业务逻辑时可以尽可能的对代码复用,从而减少很多可能会出错的几率,增加开发效率和可维护性。
// 常量
export const HOST = "127.0.0.1"
export const HELLO_MSG = '你好'
// 方法
export function wait(time) {
return new Promise(resolve => {
setTimeout(resolve, time)
})
}
// 类
export default class Vector2 {
x=null
y=null
add(){}
sub(){}
distence(){}
}
UML
什么是UML
Unified Modeling Language (UML)又称统一建模语言或标准建模语言,是始于1997年一个OMG标准,它是一个支持模型化和软件系统开发的图形化语言,为软件开发的所有阶段提供模型化和可视化支持,包括由需求分析到规格,到构造和配置。 面向对象的分析与设计(OOA&D,OOAD)方法的发展在80年代末至90年代中出现了一个高潮,UML是这个高潮的产物。它不仅统一了Booch、Rumbaugh和Jacobson的表示方法,而且对其作了进一步的发展,并最终统一为大众所接受的标准建模语言。
UML 实际上在前期设计项目数据模型时是非常有用的一套工具,个人认为在构造一个关联级超过3层以上的功能时,都应该针对这个功能抽象制作UML图,这样非常有利于后面的代码编写。正所谓,磨刀不费砍柴工。
UML类图关系
继承(Generalization)
指的是一个类继承另外一个类的功能,并可以增加自己的新功能的能力,继承是类与类或接口与接口之间最常见的关系。
实现(Realization)
指的是一个class类实现interface接口(可以是多个)的功能;实现是类与接口之间最常见的关系。
依赖(Dependency)
可以简单的理解,就是一个类A使用到了另一个类B,而这种使用关系是具有偶然性的、临时性的、非常弱的,但B类的变化会影响到A;比如某人要过河,需要借用一条船,此时人与船的关系就是依赖。
关联(Association)
他体现的是两个类,或者类与接口之间语义级别的一种强依赖关系,比如我和我的朋友,双方关系是平等的。
聚合(Aggregation)
聚合是关联关系的一种特例,他体现的是整体与部分、拥有的关系,即has-a的关系,此时整体与部分之间是可分离的,他们可以具体各自的什么周期,部分可以属于多个整体对象,也可以为多个整体对象共享;
组合(Composition)
组合也是关联关系的一种特征,他体现的是一种contains-a的关系,这种关系比聚合更强,也称强聚合;他同样体现整体与部分间的关系,但此时整体与部分是不可分的,整体的生命周期结束也就意味着部分的生命周期结束;
UML的简单应用
说了这么多,主要是简单介绍一下UML最简单的一些类图关系定义。这个在画UML图、看UML图时都非常有用!如果不了解上面的这些箭头的含义,那么是很难理解UML类图的。
我举个例子,比如现在需要构建一个房子的全部数据。
一个房子需要些什么抽象模型?楼层,房间,墙,家具,吊顶,地板,踢脚线,窗口,门,墙角等等。
只看户型信息的话有哪些内容?楼层,房间,墙,墙中门、窗所需要的洞,墙面,吊顶,地板,每层的高度,地面、吊顶、墙面所需要的铺贴材质,材质铺贴的方向,墙的长、厚,墙洞的长宽。
家具这些需要什么内容?普通家具的长宽高、位置坐标和旋转方向,组合家具的长宽高、位置坐标和旋转方向。
面对这么多复杂的数据内容,我们必须要细化到每个类中才可以实现整体的House
数据。我简单的做了一个UML图,不是很完善,但可以正常说明问题,请大家参考学习。