设计模式0.5: 与众不同的JavaScript
基于对象的JavaScript
JavaScript与很多面向对象的语言不同,它最初并没有class这个概念,ES6新增的class关键字其实也是通过函数实现的。
JavaScript的变量都是基于对象的,为什么这么说呢?
我们来看几个例子:
例1:
这里我定义了一个数,也就是一个number类型的变量,我们通过__proto__
去查询它的原型。可以看到,他是属于一个Number类的。侧面可以看出,即使一个数,也是一个对象,有自己的方法(从基类继承过来)。
例2:
let a = new Object() // a = {}
Object.defineProperties(a, {
'age': { value:12, writable:false }
})
a.age // 12
a.age = 1
a.age // 12
在这个例子中,我们使用所有类的基类Object,new了一个变量。此时这个变量里是什么都没有的,但是我们使用defineProperties
方法,就可以向变量中定义属性,而且还可以通过writable
控制是否可以更改变量值。这其实就是一个对象最基本的创建和定义属性的方法。
例3:
let str = 'abcd'
console.log(str.charAt(1)) // b
String.prototype.charAt = (num) => `你的参数是${num}`
console.log(str.charAt(1)) // 你的参数是1
我们先是定义了一个字符串,然后使用了charAt方法,这显而易见是可以的。接下来,我们更改了String类的原型prototype上的charAt方法,然后重新str.charAt(1)
而这一次,返回的东西却发生了变化。这说明了字符串也是一个对象,而且有一个原型链,用来调用基类(String)的方法。
通过new来解析原型和原型链的作用
既然JavaScript是基于对象的,那么我们要想让一个任何一个变量做一些事,那么我们就需要让对象有一些属性,这些属性从哪里来呢?答案是原型上。
str的原型上,有很多的方法,当str需要调用的时候,由于自己没有这个属性,就回去原型上找,从而实现使用String类功能的效果。
而__proto__
的绑定过程,一般是在new一个新的基类对象中完成的。
当我们执行new方法时,经历了如下步骤
a = new A('xx')
- 创建一个新对象
- 将prototype和新对象的
__proto__
(这里原型链也会起作用)连接 - 执行(构造)函数的
A.call(a,'xx')
,绑定新对象的this指向 - 返回 this (在构造函数没有返回值的情况下)
从而实现了原型的传递,形成了原型链。
JavaScript中的class
calss是在ES6中才正式出现的,class的功能其实都可以使用函数来实现,所以class也可以是说是一个函数。我们使用class定义一个类,再使用new关键字实例化。这样意义是,每次new都会返回一个新对象,使得每次在使用某一个类new时,new出的对象之间都不会互相影响。
此外,由于JavaScript中并没有面向对象中一些特性,所以在JavaScript中,只有两种类,基类和派生类。所有类的基类是Object
,当我们使用 class A extends B {}
继承时,B作为基类,A为派生类。
设计模式1:全局变量优化
情景1:
当我们需要实现一个验证用户是否是作者的功能时,我们可以用这样几个函数:
const checkId = function() {}
const checkName = function() {}
const checkWorksNum = function() {}
这样看起来没有什么问题。
但是当我们又要实现一个验证是否是读者的功能时,我们也需要定义几个重复的函数:
const checkId = function() {}
const checkName = function() {}
但是由于之前已经定义过这两个函数了,那么这两个函数就会和上面的函数混淆。
情景2:
还是上面的函数,如果我们或者其他人在之后的使用时增加了新的功能,有更改了一些功能。 那么此时由于他是一个全局变量,那么我们之前的函数就会受到后来函数的影响。
以上情景都是变量污染的例子,由于全局变量一旦定义,就可以随处调,但是也随处可以更改并影响全局,可谓是一把双刃剑。那么,对于上面的情况,我们应该怎么处理呢?
对于情景一,我们可以采用用一个对象将函数包裹起来来使用:
const checkAutor = {
checkId: function() {},
checkName: function() {},
checkWorksNum: function() {}
}
...
const checkReader = {
checkId: function() {},
checkName: function() {}
}
这样,我们之后调用检查是否是作者的方法,就可以通过调用相应的对象方法来使用了:
checkAutor.checkId()
对于场景二,我们可以采用类来使得每次调用对象的时候都是新的对象,从而实习不会互相干扰的效果。
class CheckAuthor {
checkId = function() {}
checkName = function() {}
checkWorksNum = function() {}
}
let a = new CheckAuthor()
这样我们就实现了每次调用的时候,通过new来新建对象,而不是使用同一个。
设计模式2:js单例模式
(14) js设计模式(一)-单例模式 - SegmentFault 思否
单例模式的优缺点和使用场景 - 晓明的哥哥 - 博客园 (cnblogs.com)
js前端单例模式实现与应用 - 简书 (jianshu.com)
javascript单例模式(懒汉 饿汉) - 奋斗中的小鸟 - 博客园 (cnblogs.com)
构建单例模式的几种方法:
//整个模块定义一个对象
//实例模式的三种方法
//一开始就创建一个实例(饿汉模式)
function One() {
if(!One.instance) {
this.fn = () => {
console.log('>>>>')
}
this.balabala = 'QAQ'
One.instance = this
}
return One.instance
}
//调用时才创建一个方法(懒汉模式)
function One() {
if(One.instance)
return One.instance
else {
this.fn = () => {
console.log('>>>>')
}
this.balabala = 'QAQ'
One.instance = this
}
}
//class创建
class One {
instance = null
constructor() {
this.balabala = 'hello',
this.fn = () => {
console.log('>>>>')
}
}
static getInstance = () => {
if(!this.instance) {
this.instance = new One()
}
return this.instance
}
}
//三种方法下的输出结果
const a = One.getInstance()
const b = One.getInstance()
b.balabala = 'world'
console.log(a) //One { instance: null, balabala: 'world', fn: [Function (anonymous)] }
console.log(b) //One { instance: null, balabala: 'world', fn: [Function (anonymous)] }
console.log(a === b) //true
设计模式3:工厂模式
简单工厂模式
又叫静态工厂方法,由一个工厂对象决定创建某一种产品对象类的实例。主要用来创建同一类对象。
又叫静态工厂方法,由一个工厂对象决定创建某一种产品对象类的实例。主要用来创建同一类对象。
实例:
function factory(type) {
function Atype() {
this.value = 'A'
this.view = 'Atype'
}
function Btype() {
this.value = 'B'
this.view = 'Btype'
}
function Ctype() {
this.value = 'C'
this.view = 'Ctype'
}
switch(type) {
case 'A': {
return new Atype()
break;
}
case 'B': {
return new Btype()
break;
}
case 'C': {
return new Ctype()
break;
}
}
}
const a = factory('A') // a = {value:'A',view:'Atype'}
由于上面这种写法,我们每次增加或修改类,都需要在两个地方操作,不是很方便,所以我们可以优化一下
function factoryPro(type) {
function Type(opt) {
this.value = opt.value
this.view = opt.view
}
switch(type) {
case 'A': {
return new Type({
value: 'A',
view: 'Atype'
})
break;
}
case 'B': {
return new Type({
value: 'B',
view: 'Btype'
})
break;
}
case 'C': {
return new Type({
value: 'C',
view: 'Ctype'
})
break;
}
}
}
工厂方法模式
在上述的情况下,工厂是直接生产一个类,但是如果没有这个类,我们则需要有个"兜底"提示。同时,也可以增加一个功能,灵活处理直接使用方法和new方法的两种情况。同时,我们可以不在通过函数直接创建类,而是通过增加一个
funtion factory(type) {
if(this instanceof factory){
if(this[type]) {
var a = new this[type]()
return a
}
else {
//...兜底
}
}
else{
return new factory(type);
}
}
factory.prototype = {
Atype: function() {
this.value = 'A'
this.view = 'Atype'
},
Btype: function() {
this.value = 'B'
this.view = 'Btype'
this.fn = () => {...}
}
}
抽象工厂模式
这种工厂方法其中的类是抽象类,通过子类的继承来实现实例,即function agency(subType, superType)
subType
是子类superType
是抽象类。agency方法将抽象类让子类给继承。但是我觉得使用extends
关键字也可以完成一样操作,非大型项目,这种模式用处不大。
设计模式5:外观模式(兼容模式)
对一个dom的点击事件来说,如果我们为其绑定了onclick事件,由于onclick方法是一个DOM0级的事件,当其他人又通过这种方式为当前的dom绑定了点击的事件时,相当于重复定义了一个方法,之前的onclick方法就会被覆盖。
此时我们可以采用DOM2级事件处理程序提供的方法,即时使用addEventListener实现,然后IE9之前的浏览器或一些不太常用的浏览器是不支持这种方法的,所以需要用attachEvent,而如果遇到不支持DOM2级事件处理程序兜底浏览器,就只能用onclick方法绑定事件。
为了兼容这些浏览器,可以时使用外观模式:通过定义一个更简单的高级接口,将复杂的底层接口或逻辑判断封装
//外观模式实现click绑定
function addEvent(dom, type, fn) {
if(dom.addEventListener) {
dom.addEventListener(type, fn, false) //默认在冒泡阶段执行
}else id(dom.attachEvent) {
dom.attachEvent('on' + type, fn)
}else {
dom['on' + type] = fn
}
}
通过外观模式
相关拓展
关于DOM事件流、DOM0级事件与DOM2级事件 - 云+社区 - 腾讯云 (tencent.com)
设计模式6:代理模式
不直接访问对象,而是设置一个中间对象来通过它间接访问。控制了对带访问对象的访问途径,起到了区分权限和保护对象的作用。
js 设计模式——代理模式 - 妖色调 - 博客园 (cnblogs.com)
设计模式7:装饰者模式
再不改变原对象的基础上,通过对其进行包装拓展(添加属性或者方法)使原有对象可以满足用户的更复杂需求。其要点在于如何在保证原对象不变的情况下仍然可以拓展。
例如我们可以通过装饰者模式进行onclick
方法拓展:
/**
* @description dom是需要装饰的对象,fn是需要被拓展的方法
*/
const decorator = (dom, fn) => {
if(typeof dom.onclick === 'function') {
const oldFn = dom.onclick
dom.onclick = () => {
oldFn()
fn()
}
}
else {
dom.onclick = fn
}
}
设计模式8:观察者模式
观察者模式是这样一种设计模式。一个被称作被观察者的对象,维护一组被称为观察者的对象,这些对象依赖于被观察者,被观察者自动将自身的状态的任何变化通知给它们。
在Vue,React上我们都可以看到观察者订阅者的身影。实现观察者订阅者有两种较为实用的方法:
-
Object.defineProperty()
Object.defineProperty()
方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回此对象。
Object.defineProperty(obj, 'value', {
value: 12,
get: () => {
console.log(obj.value)
},
set: (newVal) => {
console.log(newVal)
}
})
这样我们就实现了简单的对一个对象查询和设置时的监听
-
Proxy
作为Vue3代替Object.defineProperty()的对象,它实现了对数组的监听,以及对动态添加的对象的实时监听。Proxy直接劫持对象并返回新对象。性能也会有相应的优化
const handler = {
get: (obj, prop) => {
if(obj[prop])
console.log(obj[prop])
return obj[prop]
},
set: (obj, prop, value) => {
obj[prop] = value;
console.log(value)
// 表示成功
return true;
}
}
const p = new Proxy({}, handler);
设计模式9:状态模式
当一个对象的内部状态发生变化的时候,会导致行为发生改变,看起来好像是改变了对象。
举例子:当我们处理Ajax的时候,会遇到不同的statusCode,此时我们一般会通过if..else
来解决
if(res.statusCode === 200) {
...
}else if(res.statusCode === 401) {
...
}else if(res.statusCode === 403) {
...
}
但是这样不断的分支判断并不是最优写法,我们可以创建一个对象,每一种条件都作为对象内部的一种状态,面对不同的判断结果,就变成了选择对象内的一种状态。
const StateFn = function() {
let status = {
'200': function() {
console.log('状态码是200,请求成功')
//...
},
'401': function() {
//...
},
'403': function() {
//...
}
}
const show = (code) => {
status[`${code}`]() && status[`${code}`]()
}
return { show }
}
const stateFn = StateFn()
stateFn.show(200)
这里我们利用函数来创建对象,ES6后,也可以使用class关键字解决。
设计架构:MVC,MVP,MVVM
这三种都是在软件设计时的常用架构模式。
MVC
MVC模式将架构分为了三部分:
- 视图(View):用户界面。
- 控制器(Controller):业务逻辑
- 模型(Model):数据保存
其各部分间的通讯模式为:视图层传送指令给控制器;控制器执行一定的业务逻辑后,修改好了数据,再要求Model层更新数据保存;Model层将新的数据发送给视图并且得到反馈。
MVP
MVP架构的三部分:
- 视图(View):用户界面。
- 控制器(Presenter):业务逻辑
- 模型(Model):数据保存
为了避免模型与视图之间的耦合,MVP模式中模型不与视图层发生通信,而是模型与视图层全部和控制器进行双向通信
通讯模式:视图层向Presenter请求加载数据,Presenter加载数据给视图层。Model层变得很简单,只负责接收和为Presenter加载数据。
这种模式让V和M解耦,层次清晰。
MVVM
MVVM和MVP模式十分相似,唯一的不同在于,将控制器(Presenter)改变成了ViewModel层,它的作用在于实现与View视图层的双向绑定,有利于数据更新时视图层的快速更新。