目录
一. 设计模式描述
模式定义
设计模式: 在面向对象软件设计过程中针对特定问题的简洁而优雅的解决方案。
(开发实践中总结出来的针对特定问题的一套有效解决方案)
模式分类
设计模式按标准划分有3大类,共包含23中主要的类型。 由于JavaScript的一些特性, 如弱类型语言,无接口编程等特征这里主要介绍一些常用且比较重要的模式。
类型 | 模式名称 |
---|---|
创建型 | 工厂 单例 原型 |
组合型(结构型) | 适配器 装饰器 代理 外观 桥接 |
行为型 | 观察者 命令 中介者 状态 策略 解释器 迭代器 访问者 模板方法 职责链 备忘录 |
下面我们开始针对这些模式一一进行了解和深入
工厂模式
1.1 基本特征
工厂模式有三种形式: 简单工厂模式(Simple Factory), 工厂方法模式(Factory Method) 和 抽象工厂模式(Abstract Factory)。 js 中比较常用的简单工厂模式。它的设计思想即:
- 将new操作单独封装,只对外提供相应接口;
- 遇到new 时,就要考虑是否应该使用工厂模式
1.2 主要作用
工厂模式的主要作用:
- 主要用于隐藏创建实例的复杂度,只需要对外提供一个接口;
- 实现构造函数和创建者的分离,满足开发封闭的原则;
1.3 分类概括
- 简单工厂模式: 一个工厂对象创建一种产品对象实例。 即用来创建同一类对象;
- 工厂方法: 建立抽象核心类,将创建实例的实际重心放在核心抽象大类的子类中;
- 抽象工厂模式: 对类的工厂抽象用来创建类簇,不负责创建某一类产品的实例。js中基本不会用到抽象工厂模式,这里探讨前两类模式。
1.4 实例
// 定义产品
class Product {
constructor(name){
this.name = name;
}
init(){
console.log(‘初始化产品’)
}
}
// 定义工厂
class Factory{
create(name) {
return new Product(name); // 核心
}
}
let fac = new Factory();
let pro = fac.create(' product one ')
pro.init()
工厂模式最直观的地方在于,创建产品对象不是通过直接new 产品类实现,而是通过工厂方法实现。
下面一个简单工厂的例子 (解决new 问题)
class User {
constructor(opt) {
this.name = opt.name;
this.viewPage = opt.viewPage;
}
static getInstance(role) {
switch(role) {
case 'superAdmin':
return new User({name: '超级管理员', viewPage: ['首页', 'xxx',xxxxx]})
break;
case 'admin':
return new User({name: '管理员', viewPage: ['首页', ‘xxxxx’, xxxxx]})
break;
default:
throw new Error('params error')
}
}
// 调用
let superAdmin = User.getInstance('superAdmin');
let admin = User.getInstance('admin')
}
缺点: 每次有新的实例时,需要重写这个User大类。
简单工厂每次创建新的对象实例时,只需要传入相应参数,就可以得到指定的对象实例。
工厂方法模式 (将创建实例对象的过程给其子类来实现)
class User {
constructor(name='', viewPage=[]) {
this.name = name;
this.viewPage = viewPage;
}
}
class UserFactory extends User {
constructor(name, viewPage) {
super(name, viewPage)
}
create (role) {
switch( role ) {
case 'superAdmin':
return new UserFactory('超级管理员', ['首页', ‘通信’, xxxx])
break;
case 'admin':
return new UserFactory('管理员', ['首页', xxxxxx])
break;
default:
throw new Error('params error')
}
}
}
let userFactory = new UserFactory();
let superAdmin = userFactory.create('superAdmin');
let admin = userFactory.create('admin')
let user = userFactory.create('user')
1.5 应用场景
jQuery的选择器$(selector)
$(‘div’) 和 new ( ′ d i v ′ ) 有 何 区 别 ? 为 什 么 ('div') 有何区别? 为什么 (′div′)有何区别?为什么(‘div’) 就能直接实现 new 的效果,同时去除了new 书写复杂的弊端, 还能实现链式操作。 就是因为$内置的实现机制是工厂模式。 底层代码如下:
class jQuery {
constructor(selector) {
super(selector)
}
//...................
}
window.$ = function(selector) {
return new jQuery(selector)
}
Vue 异步组件
Vue.component('async-example', (resolve, reject) => {
setTimeout(function(){
resolve({
template: `<div>I am async! </div>`
})
})
})
所有,我们平时遇到需要创建实例的时候,就可以考虑是否能用工厂模式实现
单例模式
1.1 基本特征
单例模式, 即保证实例在全局的单一性。概述如下:
- 系统中被唯一使用
- 一个类只有一个实例(必须是强相等 ===)
单例模式,可以避免需要外部变量来判定是否存在的低端方法。
1.2 实例
class Dialog{
login () {
console.log('login....')
}
}
Dialog.create = (function(){
let instance
return function(){
if(!instance) {
instance = new Dialog();
}
return instance
}
})()
let m1 = Dialog.create();
let m2 = Dialog.create();
console.log(m1 === m2) // true
如上单例模式由单例对象内来提供实例的创建
还有一种方式如下:
let Dialog= (function(name){
let instance;
return function(){
if(instance) {
return instance;
}
this.name = name;
return instance = this;
}
})()
Dialog.prototype.getName = function(){
return this.name
}
let question = new Dialog('问题框')
let answer = new Dialog('回答框')
console.log(question === answer) // true
console.log(question.getName()); // '问题框'
console.log(answer.getName()); // '问题框'
单例模式实现的实质即创建一个可以返回对象实例的引用和一个获取该实例的方法。 确保创建对象引用保持唯一。
1.3 应用场景
单例模式应用场景很多, 比如Vuex 和 redux 中的store
适配器模式
1.1 基本特征
适配器模式(Adapter)是将一个类(对象)的接口(方法或属性)转化成适应当前场景的另一个接口(方法或属性),适配器模式使得原本由于接口不兼容而不能一起工作 的哪些类(对象)可以一起工作。
适配器模式必须包含目标(Target), 源(Adaptee)和适配器(Adapter) 三个角色。
1.2 应用场景
一个基于ES6类的适配器实例():
// 目标
class Target {
typeaGB(){
throw new Error(' 国内暂不支持该标准, 需要重写....')
}
}
// 源
class Adaptee{
typeHKB() {
console.log("香港标准配件")
}
}
// 适配器
class Adapter extends Target {
constructor(adaptee) {
super();
this.adaptee = adaptee
}
typeGB() {
this.adaptee.typeHKB();
}
}
let adaptee = new Adaptee();
let adapter = new Adapter(adaptee)
adapter.typeGB(); // 香港标准配件
上面例子将Adaptee 类的实例对象的typeHKB() 适配了通用的typeGB()方法。
装饰器模式
1.1 基本特征
装饰器就是在原来方法的基础上去装饰一些针对特别场景所适用的方法,即添加一些新功能。有如下特点:
- 为对象添加新功能;
- 不改变其原有的结构和功能,即原有功能还继续会用,且场景不会改变;
class Flower{
draw() {
console.log('画一朵花')
}
}
// 装饰器类
class Decorator {
constructor(flower) {
this.flower= flower
}
draw() {
this.flower.draw();
this.setFlowerRed()
}
setFlowerRed() {
console.log('给花涂上红色')
}
}
let flower = new Flower();
let decorator = new Decorator(flower);
decorator.draw(); // 画一朵花, 并涂上红色
该例中, 通过Decorator 装饰器类, 重写了实例对象的draw方法, 给其新增了setFlowerRed() 方法, 其最后输出结果进行了装饰。
1.2 装饰器插件
ES7中就存在了装饰器语法,需要安装相应的babel插件。 首先安装一下插件,并做相关的语法配置:
npm i babel-plugin-transform-decorators-legacy
//.babelrc
{
"presets": ["es2015", "latest"],
"plugins": ["transform-decorators-legacy"]
}
给一个Person类上添加一个装饰器 setName. 此时Person类就具有了装饰器赋予的属性:
@setName
class Person {}
function setName(target) {
target.name = "张三"
}
console.log(Person .name)
上面示例可得出如下结论:
@decorator
class A {}
等同于
class A{}
A = decorator(A) || A
1.3 实例场景
这里列举mixin 和 属性装饰例子。
(1) mixin 示例
function mixins(...list) {
return function(target) {
Object.assign(target.prototype, ...list)
}
}
const Foo = {
foo() {
console.log("foo")
}
}
@mixins(Foo)
class MyClass { }
let obj = new MyClass ();
obj.foo();
上例中, Foo 作为list的实参, MyClass 作为target的实参。 最终实现将Foo的所有原型方法装饰到MyClass 类上,成为MyClass 的方法。
(2) 属性装饰器
固定语法:
function readonly(target, name, descriptor) {
// descriptor 属性描述对象(Object.defineProperty 中会用到)
/*
{
value: specifiedFunction,
enumerable: false,
configurable: true
writable: true 是否可改
}
*/
}
设置类属性只读
function readonly(target , name , descriptor) {
descriptor.writable = false;
}
class Person {
constructor() {
this.first = '周';
this.last = '杰伦';
}
@readonly
name() {
return `${this.first}${this.last}`
}
}
const p = new Person();
console.log(p.name()); // 打印成功 ,‘周杰伦’
// 试图修改name:
p.name = function() {
return true;
}
// Uncaught TypeError:Cannot assign to read only property 'name' of object '#<Person>'
这里如果给属性添加了只读的装饰后,代码试图修改属性的命令将会报错。
代理模式
1.1 基本特征
为一个对象提供一个代用品或占位符,以便控制对它的访问。
白话: 代理模式突出“代理” 的含义, 其需要三类角色: 使用者,目标对象和代理者。使用者的目的是直接访问目标对象,但却不能直接访问,而是要先通过代理者。特征为:
- 使用者无权访问目标对象;
- 中间加代理,通过代理做授权和控制;
加载图片例子:
class LoadImg {
constructor(fileName) {
this.fileName = fileName;
this.loadFromDisk();
}
display() {
consoe.log('display' + this.fileName)
}
loadFromDisk() {
console.log('loading...' + this.fileName)
}
}
class ProxyImg {
constructor(fileName) {
this.loadImg = new LoadImg(fileName)
}
display() {
this.loadImg.display()
}
}
let proxyImg = new ProxyImg('xx.png');
proxyImg.display();
1.2 实际应用
(1) HTML元素事件代理:
HTML元素代理事件,即:页面代理事件。举例:
<body>
<div id="div1">
<a href="#">a1</a>
<a href="#">a2</a>
<a href="#">a3</a>
<a href="#">a4</a>
<a href="#">a5</a>
</div>
<script>
var div1 = document.getElementById('div1');
div1.addEventListener('click', (e) => {
var target = e.target;
if(target.nodeName === 'A') {
alert(target.innerHTML);
}
})
</script>
</body>
该例中,并未直接在元素上添加点击事件,而是通过监听元素点击事件,并通过定位元素节点名称来代理到标签的点击,最终利用捕获事件来实现相应的点击效果。
(2) $.proxy
$.proxy 是 jQuery 提供的一个代理方法,改一下上例再写一个点击事件:
$("#div").click(function(){
setTimeout(function(){
$(this).css('background-color','yellow')
}, 1000)
})
上例div 点击最终不会实现背景色变化,因为setTimeout 的因素,导致内部函数中的this指向的是window 而非相应div. 通常可以在setTimeout方法前获取当前this指向,代码如下:
$('#div1').click(function() {
let _this = this;
setTimeout(function() {
$(_this).css('background-color', 'yellow')
},1000)
})
而如果不用上面的方法,我们可以用$.proxy 代理目标元素来实现:
$('#div1').click(function() {
var fn = $.proxy(function() {
$(this).css('background-color', 'yellow')
}, this);
setTimeout(fn , 1000)
})
(3) ES6 proxy
ES6的 Proxy 相信大家都不会陌生,Vue3.0的双向绑定原理就依赖ES6 的 Proxy来实现,例:
let star = {
name: '晨大神',
song: '来呀,快活呀'
age: 66,
phone: 13566666666
}
let agent = new Proxy(star , {
get(target , key) {
if(key == 'phone') {
// 返回自己的电话
return 13688888888
}
if(key == 'price') {
return 60000000000
}
return target[key]
},
set(target , key , val) {
if(key === 'customPrice') {
if(val < 200000000) {
throw new Error('价格低了')
}
else {
target[key] = value;
return true
}
}
}
})
// agent 对象会根据相应的代理规则,执行相应的操作:
agent.phone // 15667096303
agent.price // 60000000000
观察者模式
1.1 基本特征
观察者模式运用的比较多,也是一个很重要的设计模式之一。 观察者模式,也叫订阅-发布模式。 Vue中,该模式定义了一种一对N的关系,使观察者同时监听某一个对象相应的状态变换,一旦变化则通知到所有观察者,从而触发观察者相应的事件。 因此,观察者模式中的角色有两类: 观察者(发布者) 和 被观察者(订阅者)。
这里摘用网上的一个观察者模式UML类图:
类图解析:
- 每个观察者(Observer) 都有一个update方法,并且观察者的状态就是等待被触发;
- 每个主题(subject)都可以通过attach 方法接纳N个观察者所观察,即观察者们在主题的observers数组里;
- 主题有初始化状态(init), 获取状态(getState)和设置状态(setState) 三个通用型方法;
- 当主题的状态发生变化时,通过特定的notifyAllObervers 方法通知所有观察者。
针对上述描述举个例子:
// 创建一个主题, 保存状态,状态变化之后触发所有观察者对象
class Subject {
constructor() {
this.state = 0;
this.observers = []
}
getState() {
return this.state
}
setState(state) {
this.state = state;
this.notifyAllObservers()
}
notifyAllObservers() {
this.observers.forEach(observer => {
observer.update()
})
}
attach(observer) {
this.observers.push(observer)
}
}
// 观察者
class Observer {
constructor(name, subject) {
this.name = name;
this.subject = subject;
this.subject.attach(this);
}
update() {
console.log(`${this.name} update, state: ${this.subject.getState()}`)
}
}
let sub = new Subject();
let obs1 = new Observer('o1', sub);
let obs2 = new Observer('o2', sub);
let obs3 = new Observer('o3', sub);
sub.setState(1)
sub.setState(2)
sub.setState(3)
/*
输出如下:
o1 update, state: 1
o2 update, state: 1
o3 update, state: 1
o1 update, state: 2
o2 update, state: 2
o3 update, state: 2
o2 update, state: 3
o3 update, state: 3
*/
最终可以看到主题每次改变状态后都会触发所有观察者状态更新。
1.2 应用场景
观察者模式在平时不经意间就会用到,比如:Promise 等,Node.js 中的EvnetEmitter事件监听器, Vue 的Watch生命周期钩子等等。比如在Vue组件生命周期Watch, 在Watch里设定了数据监听,一旦监听的数据改变了就会触发相应事件; 还有Promise, 为什么异步操作得到结果后就会进入到then或者catch里? 这些都是依赖于观察者模式
目前已将23种设计模式中比较常用及核心的简单总结分析了一遍。 剩下的一些模式后续有空再梳理出来
未完待续…