# 设计模式-总览
【参考】 慕课网-Javascript 设计模式系统讲解与应用 (opens new window)
# 一、导学
# 论工程师的设计能力
- 3年工作经验,面试必考设计能力
- 成为项目技术负责人,设计能力是必要基础
- 从写好代码,到做好设计,设计模式是必经之路
# 前端学习设计模式的困惑
- 网上的资料都是针对 Java 等后端语言的
- 看懂概念,但是不知道怎么用,看完就忘
- 现在的 JS 框架如vue、react封装的很完善,基本接触不到到底都用了哪些设计模式
# 课程概述
- 做什么?—— 讲解JS设计模式
- 哪些部分? —— 面向对象、设计原则、设计模式
- 技术? —— 面向对象,UML类图,ES6
# 知识点介绍
- 面向对象:ES class语法、三要素、UML类图
- 设计原则:何为设计、5大设计原则、从设计到模式
- 设计模式:分优先级讲解、结合核心技术、结合框架应用
- 综合示例:设计方案、代码演示、设计模式对应关系的分析
# 课程安排
# 1、面向对象
- 使用 webpack 和 babel 搭建ES6编译环境
- ES6 class 面向对象的语法
- 面向对象三要素:继承 封装 多态
# 2、设计原则
- 通过 《Linux/Unix设计哲学》理解何为设计
- 5大设计原则分析和理解,以及代码演示
- 设计模式 -> 从设计到模式
# 3、设计模式
- 概述:创建型、结构型、行为型
- 常用设计模式,详细讲解,结合经典使用场景
- 非常用设计模式,理解概念,示例演示
- 有主有次,掌握重点
# 4、综合示例
- 用jQuery实现一个简单的购物车
- 设计分析,画UML类图
- 代码演示
- 总结使用的7种设计模式
# 讲授方式
- 先基础后实践,先设计后模式
- 重点、常用的设计模式,配合经典使用场景
- 综合示例,演示设计模式如何使用
- 用JS的方式讲解面向对象和设计模式
# 课程收获
- 面向对象思想,UML类图
- 5大设计原则,23种设计模式
- 能应对前端面试中相关的面试题
- 提升个人的设计能力
# 学习前提
- 了解面向对象,能熟练使用jQuery或类似工具库
- 有ES6语法基础,用过nodejs和npm环境
- 了解vue和react(至少看过文档,做过demo)
# 重点提示
- 本课讲解设计模式,不是实战项目也不是源码分析
- 23种设计模式不是都常用,分清主次
- 设计模式在JS和Java中的讲解方式有区别
- 不适合刚入门编程的同学,参考上文的学习前提
# 二、面向对象
# 概念
- 类
- 对象(实例)
# 三要素
- 继承:子类继承父类
- 封装:数据的权限和保密
- 多态:同一接口不同实现
# 继承
作用:继承可以将公共方法抽离出来,提高复用,减少冗余
# 封装
- public完全开放
- protected对子类开放
- private对自己开放
作用:减少耦合,不该外漏的不外漏,利于数据、接口的权限管理 (ES6目前不支持,可使用TS实践)
# 多态
作用:保持子类的开放性和灵活性,面相接口编程
JS使用极少,了解即可
# 为何使用面向对象?
原因:结构化的事务容易管理
- 程序结构化:顺序、分支、循环
- 数据结构化:面相对象
# UML类图
UML:Unified Modeling Language 统一建模语言
UML 包含很多的图,类图是其中一个
关系、泛化、关联
# 三、设计原则
# 何为设计?
- 描述
- 按照哪一种思路或者标准来实现功能
- 功能相同,可以有不同设计方案来实现
- 伴随着需求增加,设计的作用才能体现出来
- 《Unix/Linux设计哲学》
- 准则1:小即是美
- 准则2:让每个程序员只做一件事
- 准则3:快速建立原型
- 准则4:舍弃高效率而取可移植性
- 准则5:采用纯文本来存储数据
- 准则6:充分利用软件的杠杆效应(复用)
- 准则7:使用shell脚本来提高杠杆效应和可移植性
- 准则8:避免强制性的用户界面
- 准则9:让每个程序都成为过滤器
- 小准则:允许用户定制环境
- 小准则:尽量使操作系统内核小而轻量化
- 小准则:使用小写字母并尽量简写
- 小准则:沉默是金(异常时不输出)
- 小准则:各部分之和大于整体
- 小准则:寻求90%的解决方案
# 五大设计原则
SOLID五大设计原则
- S - 单一职责原则
- O - 开放封闭原则
- L - 李氏置换原则
- I - 接口独立原则
- D - 依赖倒置原则
# S-单一职责原则
描述:
- 一个程序只做好一件事
- 如果功能过于复杂就拆分开,每个部分保持独立
# O-开放封闭原则
描述:
- 对扩展开放,对修改封闭
- 增加需求时,扩展新代码,而非修改已有代码
- 这是软件设计的终极目标
# L-李氏置换原则
- 子类能覆盖父类
- 父类能出现的地方子类就能出现
- JS中使用较少(弱类型&继承使用较少)
# I-接口独立原则
- 保持接口的单一独立,避免出现“胖接口”
- JS中没有接口(TS例外),使用较少
- 类似于单一职责原则,这里更关注接口
# D-依赖倒置原则
- 面相接口编程,依赖于抽象而不依赖与具体
- 使用方只关注接口而不关注具体类的实现
- JS中使用较少(没有接口&弱类型)
# 设计原则总结
- S O 体现较多,详细介绍
- L I D 体现较少,但是要了解其用意
# 从设计到模式
- 设计
- 模式
- 分开
- 从设计到模式
# 23种设计模式
- 创建型
- 工厂模式(工厂方法模式,抽象工厂模式,建造者模式)
- 单例模式
- 原型模式
- 组合型
- 适配器模式
- 装饰器模式
- 代理模式
- 外观模式
- 桥接模式
- 组合模式
- 享元模式
- 行为型
- 策略模式
- 模板方法模式
- 观察者模式
- 迭代器模式
- 职责链模式
- 命令模式
- 备忘录模式
- 状态模式(状态机)
- 访问者模式
- 中介者模式
- 解释器模式
# 如何学习设计模式?
- 明白每个设计模式的道理和用意
- 通过经典应用体会它的真正使用场景
- 自己编码时多思考,尽量模仿(刻意训练)
# 四、工厂模式
# 介绍
将new操作单独封装,遇到new时,就要考虑是否该使用工厂模式
# 示例
- 购买汉堡,直接点餐、取餐,不会自己亲手做;
- 商店要封装做汉堡的工作,做好直接给消费者
# 场景
- jQuery,
$('div')
,支持链式操作,将jQuery名字封装起来 - React.createElement 创建vnode
# 阅读源码的意义
- 学习功能实现
- 学习设计思路
- 强制自己写代码,模拟刻意训练
- 自己写出优秀的代码
# jQuery
class jQuery {
constructor(selector) {
}
}
window.$ = function(selector) {
return new jQuery(selector)
}
1
2
3
4
5
6
7
8
2
3
4
5
6
7
8
# React.createElement
class Vnode(tag, attrs, children) {
// 省略内部代码...
}
React.createElement = function(tag, attrs ,children) {
return new Vnode(tag, attrs, children)
}
1
2
3
4
5
6
2
3
4
5
6
# Vue异步组件
Vue.component('async-example', function(resolve, reject) {
setTimeout(() => {
resolve({
template: '<div>I am async!</div>'
})
}, 1000)
})
1
2
3
4
5
6
7
2
3
4
5
6
7
# 设计原则验证
- 构造函数和创建者分离
- 符合开放封闭原则
# 五、单例模式
# 介绍
一个类只有一个实例
# 示例
登录框、购物车
# 代码演示
Java使用单例模式
public class SingleObject {
private SingleObject(){ // private约束不能在外部使用构造函数
}
private SignleObject instance = null;
public SingleObject getInstance() {
if (instance == null) {
// 只new一次
instance = new SingleObject();
}
return instance;
}
public void login(username, password) {
System.out.println('login...')
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
JS使用单例模式(靠文档注释约束)
class SingleObject {
login() {
console.log('login...')
}
}
SingleObject.getInstance = (function() {
let instance
return function() {
if (!instance) {
instance = new SingleObject();
}
return instance
}
})()
// 测试:注意这里只能使用静态函数 getInstance,不能 new SingleObject()!!!
let obj1 = SingleObject.getInstance()
obj1.login();
let obj2 = SingleObject.getInstance()
obj2.login();
console.log(obj1 === obj2)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# 场景
- jQuery只有一个$
- 模拟登录框(购物车)
- vuex 和 redux 中的 store
# jQuery只有一个$
if (window.jQuery != null) {
return window.jQuery
} else {
// 初始化...
}
1
2
3
4
5
2
3
4
5
# 设计原则验证
- 符合单一职责原则,只实例化唯一的对象
# 六、适配器模式
# 介绍
旧接口格式和使用者不兼容,中间加一个适配转换接口
# 示例
- 插头转接口
- client -> target(adapter)
# 场景
- 封装旧接口
- vue 的 computed
# 设计原则验证
- 将旧接口和使用者进行分离
- 符合开放封闭原则
# 七、装饰器模式
# 介绍
为对象添加新功能,不改变其原有的结构和功能
# 示例
- 手机壳
# URL类图和代码演示
# 场景 ES7 装饰器
/**
* 装饰器的原理
**/
@decorator
class A {}
// 等同于
class A {}
A = decorator(A) || A;
1
2
3
4
5
6
7
8
9
10
2
3
4
5
6
7
8
9
10
# 场景 core-decorators第三方库
- 第三方开源lib
- 提供常用的装饰器
import { deprecate } from 'core-decorators'
class Person {
@deprecate('即将废弃', {url: 'www.baidu.com'})
name() {
return 'Aaron'
}
}
let p = new Person();
p.name();
1
2
3
4
5
6
7
8
9
10
11
2
3
4
5
6
7
8
9
10
11
# 设计原则验证
- 将现有对象和装饰器进行分离,两者独立存在,符合开放封闭原则
# 八、代理模式
# 介绍
- 使用者无权访问目标对象
- 中间加代理,通过代理做授权和控制
- 用户 - 代理对象 - 目标对象
# 示例
- 科学上网
- 明星经纪人
# 传统UML类图&演示
Client(proxyImg) -> ProxyImg(realImg/display) -> RealImg(fileName/display)
# 场景
- 网页事件代理
- jQuery $.proxy
- ES6 Proxy
# 网页事件代理
let div1 = document.getElementById('div1')
div1.addEventListener('click', function(e) {
var target = e.target
if (target.nodeName === 'A') {
alert(target.innerHTML)
}
})
1
2
3
4
5
6
7
2
3
4
5
6
7
# $.proxy
# ES6 Proxy
// 明星
let star = {
name: 'Aaron',
age: 31,
phone: 'star_1381809xxxx'
}
// 经纪人
let agent = new Proxy(star, {
get: function(target, key) {
if (key === 'phone') {
// 返回经纪人自己的手机号
return 'agent_13800000000'
}
if (key === 'price') {
// 明星不保价,经纪人报价
return '报价_120000'
}
return target[key]
},
set: function(target, key, val) {
if (key === 'customPrice') {
if (val < 100000) {
throw new Error('价格太低')
} else {
target[key] = val
return true
}
}
}
})
console.log(agent.name)
console.log(agent.age)
console.log(agent.phone)
console.log(agent.price)
agent.customPrice = 200000
console.log('agent.customPrice', agent.customPrice)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
# 设计原则验证
- 代理类和目标类分离,隔离开目标类和使用者
- 符合开放封闭原则
# 代理模式 vs 适配器模式
- 适配器模式:提供一个不同的接口(如不同版本的插头)
- 代理模式:提供一模一样的接口
# 代理模式 vs 装饰器模式
- 装饰器模式:扩展功能,原有功能不变且可直接使用
- 代理模式:显示原有功能,但是经过限制或者阉割之后的
# 示例对比
- 代理模式 —— 经纪人
- 适配器模式 —— 插头转换器
- 装饰器模式 ——手机壳
# 九、外观模式
# 介绍
为子系统中的一组接口提供了一个高层接口,使用者使用这个高层接口
# 示例
- 去医院看病,接待员去挂号、缴费、看诊、取药
# UML类图
Client -> Facade -> (SubSystemA/SubSystemB/SubSystemC)
# 十、观察者模式
# 介绍
- 发布 & 订阅
- 支持一对多
# 示例
- 订报纸、订牛奶
# UML类图
# 代码演示
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 s = new Subject()
let o1 = new Observer('o1', s)
let o2 = new Observer('o2', s)
let o3 = new Observer('o3', s)
s.setState(1)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
# 场景
- 网页事件绑定
- Promise(pending状态变化为fulfilled时,调用resolved处理函数)
- nodejs自定义事件(底层eventEmitter、api如fs.createReadSteam)
- nodejs处理http请求
- vue react 组件生命周期触发
- vue watch 实现
# 设计原则验证
- 主题和观察者分离,不是主动触发而是被动监听,两者解耦
- 符合开放封闭原则
# 十一、迭代器模式
# 介绍
- 顺序访问一个有序集合(非对象)
- 使用者无需知道集合的内部结构(封装)
# 示例
- Array.prototype.forEach
- for语句
- $.each()
# UML类图
# 演示
class Iterator {
constructor(container) {
this.list = container.list
this.index = 0
}
next() {
if (this.hasNext()) {
return this.list[this.index++]
}
return null
}
hasNext() {
return this.index < this.list.length
}
}
class Container {
constructor(list) {
this.list = list
}
getIterator() {
return new Iterator(this)
}
}
let arr = [1,2,3,4]
let container = new Container(arr)
let iterator = container.getIterator()
while(iterator.hasNext()) {
console.log(iterator.next())
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
# 场景
- jQuery each
- ES6 Iterator
# ES6 Iterator为何存在?
- ES6 语法中,有序集合的数据类型已经有很多
- Array Map Set String TypedArray arguments NodeList
- 需要有一个统一的遍历接口来遍历所有数据类型
- 注意:Object不是有序集合,可以用Map代替
# ES6 Iterator是什么
- 以上数据类型,都有
[Symbol.iterator]
属性 - 属性值是函数,执行函数返回一个迭代器
- 这个迭代器就有 next 方法可顺序迭代子元素
- 可运行
Array.prototype[Symbol.iterator]
来测试
# ES6 Iterator实例
// `Symbol.iterator` 并不是人人都知道,也不是每个人都需要封装一个each方法
// 因此有了 `for...of` 语法
function each(data) {
// 带有遍历器特性的对象:data[Symbol.iterator] 有值
// for (let item of data) {
// console.log(item)
// }
let iterator = data[Symbol.iterator]()
// console.log(iterator.next())
// console.log(iterator.next())
let item = {done: false}
while(!item.done) {
item = iterator.next()
if (!item.done) {
console.log(item.value)
}
}
}
let arr = [1,2,3,4]
let nodeList = document.getElementsByTagName('p')
let m = new Map()
m.set('a', 100)
m.set('b', 200)
each(arr)
each(nodeList)
each(m)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
# 设计原则验证
- 迭代器对象和目标对象分离
- 迭代器将使用者与目标对象隔离开
- 符合开放封闭原则
# 十二、状态模式
# 介绍
- 一个对象有状态变化
- 每次状态变化都会触发一个逻辑
- 不能总是用 if...else 来控制
# 示例
- 交通信号灯不同颜色的变化
# UML类图
State <-- Context
# 场景
- 有限状态机
- promise
# 十三、其他设计模式
# 原型模式
- 介绍:clone自己
- 场景:Object.create
# 桥接模式
- 介绍:用于把抽象化与实现化解耦,使得二者能够独立变化
- 场景:Canvas绘图
# 面试相关(重要)
能说出课程重点讲解的设计模式即可
设计模式:
- 设计 => 模式
- 5大设计原则 SOLID
- Single 单一职责原则
- Open-Close 开放封闭原则
- Lee 里氏替换原则
- Interface 接口独立原则
- Dependence 依赖倒置原则
- 23个设计模式
- 创建型
- 组合型
- 行为型
- 前端常见设计模式
- 工厂模式
- jQuery,
$('div')
,支持链式操作,将jQuery名字封装起来 - React.createElement 创建vnode
- jQuery,
- 单例模式
- jQuery只有一个$
- 模拟登录框(购物车)
- vuex 和 redux 中的 store
- 原型模式
- JS 的 Object.create
- 适配器模式
- vue 的 computed
- 代理模式
- ES6 Proxy
- 观察者模式
- 网页事件绑定
- Promise(pending状态变化为fulfilled时,调用resolved处理函数)(状态模式)
- vue watch 实现
- 迭代器模式
- ES6 Iterator(
[Symbol.iterator]
属性) - ES6 Iterator为何存在?
- ES6 语法中,有序集合的数据类型已经有很多
- Array Map Set String TypedArray arguments NodeList
- 需要有一个统一的遍历接口来遍历所有数据类型
- 注意:Object不是有序集合,可以用Map代替
- ES6 Iterator是什么
- 以上数据类型,都有
[Symbol.iterator]
属性 - 属性值是函数,执行函数返回一个迭代器
- 这个迭代器就有 next 方法可顺序迭代子元素
- 可运行
Array.prototype[Symbol.iterator]
来测试
- 以上数据类型,都有
- ES6 Iterator(
- 观察者模式和发布订阅模式的区别
- 观察者模式中,Subject 和 Observer直接绑定,没有中间媒介,如 addEventListener 绑定事件
- 发布订阅模式中,Publisher 和 Observer 互不相识,需要中间媒介 Event channel,如 EventBus 自定义事件
- 工厂模式
# 日常使用
- 重点讲解的设计模式,要强制自己模仿、掌握
- 非常用的设计模式,视业务场景选择性使用