# designMode
**Repository Path**: DZHgino/design-mode
## Basic Information
- **Project Name**: designMode
- **Description**: 前端常用的7种设计模式
- **Primary Language**: Unknown
- **License**: Not specified
- **Default Branch**: master
- **Homepage**: None
- **GVP Project**: No
## Statistics
- **Stars**: 0
- **Forks**: 0
- **Created**: 2025-01-09
- **Last Updated**: 2025-01-17
## Categories & Tags
**Categories**: Uncategorized
**Tags**: None
## README
# 01-工厂模式
## 开始
(图文)
### 主要内容
- 概念介绍 + 解决的问题
- UML 类图 + 代码演示
- 应用场景
### 学习方法
- UML 类图和代码结合理解
- 要结合使用场景
### 注意事项
- 遇到 `new class` 时,考虑工厂模式
## 介绍
创建对象的一种方式。不用每次都亲自创建对象,而是通过一个既定的“工厂”来生产对象。
### 示例
现在你要得到一个汉堡,你是跟服务员要(买)一个,还是自己动手做一个?这个问题,服务员就是工厂方法,而动手做一个其实就是`new A()`。
另外从快餐店考虑,你想要提供一个汉堡,是让服务员(工厂方法)做出来(`new A()`)给客户,还是让客户自己做一个汉堡?
从这个示例很容易理解工厂模式的用意,**所有的设计模式都是很讲道理的,很容易理解**
### 伪代码
OOP 中,默认创建对象一般是 `new class` ,但一些情况下用 `new class` 会很不方便。
```js
// 伪代码
let f1
class Foo {}
if (a) {
f1 = Foo(x)
}
if (b) {
f2 = Foo(x, y)
}
```
此时就需要一个“工厂”,把创建者和 class 分离,符合开放封闭原则。
```js
// 工厂
function create(a, b) {
if (a) {
return Foo(x)
}
if (b) {
return Foo(x, y)
}
}
const f1 = create(a, b)
```
### 注意
工厂模式可以拆分为三个:
- 工厂方法模式
- 抽象工厂模式
- 建造者模式
前端用不到这么细致,只需要掌握核心的工厂模式即可。
## 演示
### 标准的工厂模式

```ts
interface IProduct {
name: string
fn1: () => void
fn2: () => void
}
class Product1 implements IProduct {
name: string
constructor(name: string) {
this.name = name
}
fn1() {
alert('product1 fn1')
}
fn2() {
alert('product1 fn2')
}
}
class Product2 implements IProduct {
name: string
constructor(name: string) {
this.name = name
}
fn1() {
alert('product2 fn1')
}
fn2() {
alert('product2 fn2')
}
}
class Creator {
create(type: string, name: string): IProduct {
if (type === 'p1') {
return new Product1(name)
}
if (type === 'p2') {
return new Product2(name)
}
throw new Error('Invalid type')
}
}
```
### 简单的工厂模式

```ts
class Product {
name: string
constructor(name: string) {
this.name = name
}
fn1() {
alert('product fn1')
}
fn2() {
alert('product fn2')
}
}
class Creator {
create(name: string): Product {
return new Product(name)
}
}
```
### 是否符合设计原则?
5 大设计原则中,最重要的就是:**开放封闭原则**,对扩展开放,对修改封闭
- 工厂和类分离,解耦
- 可以扩展多个类
- 工厂的创建逻辑也可以自由扩展
## 场景
### jQuery `$('div')`
```ts
// 扩展 window.$
declare interface Window {
$: (selector: string) => JQuery
}
class JQuery {
selector: string
length: number
constructor(selector: string) {
const domList = Array.prototype.slice.call(document.querySelectorAll(selector))
const length = domList.length
for (let i = 0; i < length; i++) {
this[i] = domList[0]
}
this.selector = selector
this.length = length
}
append(elem: HTMLElement): JQuery {
// ...
return this
}
addClass(key: string, value: string): JQuery {
// ...
return this
}
html(htmlStr: string): JQuery | string {
if (htmlStr) {
// set html
return this
} else {
// get html
const html = 'xxx'
return html
}
}
}
window.$ = (selector) => {
return new JQuery(selector)
}
```
做一个对比,如果开放给用户的不是`$`,然后让用户自己去`new JQuery(selector)`,带来的问题:
- 不方便链式操作,如`$('div').append($('#p1')).html()`
- 不宜将构造函数暴露给用户,尽量高内聚、低耦合
### Vue `_createElementVNode`
在线编译 https://vue-next-template-explorer.netlify.app/
```html
静态文字
{{ msg }}
```
会编译出很多 `_createXxx` JS 代码。这些就是工厂函数,创建 vnode 。
```js
export function render(_ctx, _cache, $props, $setup, $data, $options) {
return (_openBlock(), _createElementBlock("div", null, [
_createElementVNode("span", null, "静态文字"),
_createElementVNode("span", {
id: _ctx.hello,
class: "bar"
}, _toDisplayString(_ctx.msg), 9 /* TEXT, PROPS */, ["id"])
]))
}
```
PS:不了解 Vue 模板编译流程的,可以去参考课程 https://coding.imooc.com/class/419.html
### React `createElement`
在线编译 https://www.babeljs.cn/repl
在 React 中使用 JSX 语法
```jsx
const profile =
{[user.firstName, user.lastName].join(' ')}
```
这是一种语法糖,编译之后就会是
```js
// 返回 vnode
const profile = React.createElement("div", null,
React.createElement("img", { src: "avatar.png", className: "profile" }),
React.createElement("h3", null, [user.firstName, user.lastName].join(" "))
);
```
其实`React.createElement`也是一个工厂,模拟代码
```js
class Vnode(tag, attrs, children) {
// ...省略内部代码...
}
React.createElement = function (tag, attrs, children) {
return new Vnode(tag, attrs, children)
}
```
PS:不了解 Vue 模板编译流程的,可以去参考课程 https://coding.imooc.com/class/419.html
### 总结
工厂模式在前端 JS 中应用非常广泛,随处可见
- jQuery `$`
- Vue `_createElementVNode`
- React `createElement`
## 总结
(图文)
日常项目开发中,遇到 `new class` 的场景,要考虑是否可用工厂模式。
### 内容回顾
- 概念介绍 + 解决的问题
- UML 类图 + 代码演示
- 应用场景
### 重要细节
- TS 扩展 window 属性
- 对 vnode 的理解
### 注意事项
- 遇到 `new class` 时,考虑工厂模式
## 作业
### 题目
根据课程的代码演示,自己写出代码演示,并动手画出 UML 类图。
### 提示
不要照抄,要自己动手默写、默画。
要根据自己的理解去写,而不是死记硬背。
不要求和课程完全一致,只要符合工厂模式即可。
# 02-单例模式
## 开始
(图文)
### 主要内容
- 概念介绍 + 解决的问题
- UML 类图和代码演示
- 应用场景
### 学习方法
- UML 类图和代码结合理解
- 要结合使用场景
### 注意事项
- 前端用到严格的单例模式并不多,但单例模式的思想到处都有
## 介绍
单例模式,即对一个 class 只能创建一个实例,即便调用多次。
### 示例
如一个系统的登录框、遮罩层,可能会被很多地方调用,但登录框只初始化一次即可,以后的直接复用。
再例如,想 Vuex Redux 这些全局数据存储,全局只能有一个实例,如果有多个,会出错的。
### 伪代码
登录框,初始化多次没必要。
```js
class LoginModal { }
// modal1 和 modal2 功能一样,没必要初始化两次
const modal1 = new LoginModal()
const modal2 = new LoginModal()
```
全局存储,初始化多个实例,会出错。
```js
class Store { /* get set ... */ }
const store1 = new Store()
store1.set(key, value)
const store2 = new Store()
store2.get(key) // 获取不到
```
## 演示

### 使用 TS 特性
- `static` 静态属性和方法 —— **详细介绍一下,对比“静态xx”和“实例xx”**
- `private` 外部无法直接初始化
```js
class Singleton {
// private - 外部无法初始化
private constructor() { }
// static 属性
private static instance: Singleton | null
// static 方法
static getInstance(): Singleton {
// 这里也可以写 `this.instance` ,注意和实例方法中 this 的区别!!!
if (Singleton.instance == null) {
Singleton.instance = new Singleton()
}
return Singleton.instance
}
}
// const s1 = new Singleton() // 直接初始化会报错
// Singleton.instance // 直接访问 instance 也会报错
// 创建实例
const s1 = Singleton.getInstance()
const s2 = Singleton.getInstance()
console.log(s1 === s2) // true
```
### 不使用 TS 特性
最常见的方式,使用闭包
```js
function genGetInstance() {
let instance // 闭包
class Singleton {}
return () => {
if (instance == null) {
instance = new Singleton
}
return instance
}
}
const getInstance = genGetInstance()
const s1 = getInstance()
const s2 = getInstance()
```
结合模块化语法,会更好一些
```js
let instance // 闭包
class Singleton {}
// 外部只能 import 这个函数
export default () => {
if (instance == null) {
instance = new Singleton
}
return instance
}
```
### 是否符合设计原则?
5 大设计原则中,最重要的就是:**开放封闭原则**,对扩展开放,对修改封闭
- 内部封装 getInstance ,内聚,解耦
### 注意事项
JS 是单线程语言,如果是 Java 等多线程语言,单例模式需要加**线程锁**。
## 场景
### 登录框
一个页面有很多地方调用登录框,使用单例模式
```ts
class LoginForm {
private state: string = 'hide' // 'hide' / 'show'
private constructor() {}
show() {
if (this.state === 'show') {
console.log('已经显示了')
return
}
console.log('显示 LoginForm')
this.state = 'show'
}
hide() {
if (this.state === 'hide') {
console.log('已经隐藏了')
return
}
console.log('隐藏 LoginForm')
this.state = 'hide'
}
private static instance: LoginForm | null = null
static getInstance(): LoginForm {
// 注意这里的 this
if (this.instance == null) this.instance = new LoginForm()
return this.instance
}
}
const loginForm1 = LoginForm.getInstance()
const loginForm2 = LoginForm.getInstance()
```
### 其他
前端用到严格的单例模式并不多,但单例模式的思想到处都有
- 自定义事件 eventBus 全局只有一个
- Vuex Redux store 全局只有一个
## 总结
(图文)
### 内容回顾
- 概念介绍 + 解决的问题
- UML 类图和代码演示
- 应用场景
### 重要细节
- TS `static` `private` ,以及 UML 图的表示
- 静态方法中的 `this`
- Java 多线程,单例模式需要加线程锁 —— JS 单线程
### 注意事项
- 前端用到严格的单例模式并不多,但单例模式的思想到处都有
## 作业
### 题目
根据课程代码演示,把其中的 Typescript 代码演示再自己写一遍。
### 提示
不要照抄,要自己动手默写。要根据自己的理解去写,而不是死记硬背。
不要求和课程完全一致,只要符合单例模式即可。
PS:要深刻理解代码中的 `static` 和 `private` 。
# 03-观察者模式
## 开始
(图文)
### 主要内容
- 概念介绍 + 解决的问题
- UML 类图和代码演示
- 场景
- 观察者模式 vs 发布订阅模式
### 学习方法
- 结合场景理解
- UML 要结合代码
### 注意事项
- 观察者模式很重要,本章内容较多,耐心学习
- 观察者模式场景很多,要抓住重点,不要拘泥细节
## 介绍
观察者模式是前端最常用的一个设计模式,也是 UI 编程最重要的思想。
### 示例
例如你在星巴克点了咖啡,此时你并不需要在吧台坐等,你只需要回到位子上玩手机,等咖啡好了服务员会叫你。不光叫你,其他人的咖啡好了,服务员也会叫他们来取。
还有,DOM 事件就是最常用的观察者模式
```html
```
还有,Vue React 的生命周期,也是观察者模式

## 演示
Subject 和 Observer 是**一对多**的关系

```ts
// 主题
class Subject {
private state: number = 0
private observers: Observer[] = []
getState(): number {
return this.state
}
setState(newState: number) {
this.state = newState
this.notify()
}
// 添加观察者
attach(observer: Observer) {
this.observers.push(observer)
}
// 通知所有观察者
private notify() {
for (const observer of this.observers) {
observer.update(this.state)
}
}
}
// 观察者
class Observer {
name: string
constructor(name: string) {
this.name = name
}
update(state: number) {
console.log(`${this.name} update, state is ${state}`)
}
}
const sub = new Subject()
const observer1 = new Observer('A')
sub.attach(observer1)
const observer2 = new Observer('B')
sub.attach(observer2)
sub.setState(1) // 更新状态,触发观察者 update
```
### 是否符合设计原则?
5 大设计原则中,最重要的就是:**开放封闭原则**,对扩展开放,对修改封闭
- Observer 和 Target 分离,解耦
- Observer 可自由扩展
- Target 可自由扩展
## 场景
观察者模式在前端(包括其他 UI 编程领域)应用非常广泛。
### DOM 事件
```html
```
### Vue React 组件生命周期
PS:当你开发自己的 lib 时,也要考虑它的完整生命周期,如 [wangEditor](https://www.wangeditor.com/v5/guide/editor-config.html#oncreated),负责创建,也得复杂销毁。

### Vue watch
```js
// Vue 组件配置
{
data() {
name: '双越'
},
watch: {
name(newVal, val) {
console.log(newValue, val)
}
}
}
```
PS:面试题 watch 和 watchEffect 有什么区别?—— 请看我的面试课程
### Vue 组件更新过程
PS:React 组件更新过程不是这样的,它是通过 `setState` 主动触发的,而非响应式监听。

### 各种异步的回调
#### 定时器
setTimeout setInterval
#### Promise then 回调
参考之前 `loadImg` 代码
#### nodejs stream
```js
const fs = require('fs')
const readStream = fs.createReadStream('./data/file1.txt') // 读取文件的 stream
let length = 0
readStream.on('data', function (chunk) {
length += chunk.toString().length
})
readStream.on('end', function () {
console.log(length)
})
```
#### nodejs readline
```js
const readline = require('readline');
const fs = require('fs')
const rl = readline.createInterface({
input: fs.createReadStream('./data/file1.txt')
})
let lineNum = 0
rl.on('line', function(line){
lineNum++
})
rl.on('close', function() {
console.log('lineNum', lineNum)
})
```
#### nodejs http server 回调
```js
const http = require('http')
function serverCallback(req, res) {
console.log('get 请求不处理', req.url)
res.end('hello')
}
http.createServer(serverCallback).listen(8081)
console.log('监听 8081 端口……')
```
### MutationObserver
HTML 代码
```html
```
JS 代码
```ts
function callback(records: MutationRecord[], observer: MutationObserver) {
for (let record of records) {
console.log('record', record)
}
}
const observer = new MutationObserver(callback)
const containerElem = document.getElementById('container')
const options = {
attributes: true, // 监听属性变化
attributeOldValue: true, // 变化之后,记录旧属性值
childList: true, // 监听子节点变化(新增删除)
characterData: true, // 监听节点内容或文本变化
characterDataOldValue: true, // 变化之后,记录旧内容
subtree: true, // 递归监听所有下级节点
}
// 开始监听
observer.observe(containerElem!, options)
// 停止监听
// observer.disconnect()
```
### 总结
- DOM 事件
- 组件生命周期
- Vue 组件更新过程
- 异步回调
- MutationObserver
注意,这里没有自定义事件,这个会在“发布订阅模式”讲解。
## vs 发布订阅模式
发布订阅模式,没有在传统 23 种设计模式中,它是观察者模式的另一个版本。
```js
// 绑定
event.on('event-key', () => {
// 事件1
})
event.on('event-key', () => {
// 事件2
})
// 触发执行
event.emit('event-key')
```
### 观察者模式 vs 发布订阅模式

观察者模式
- Subject 和 Observer 直接绑定,中间无媒介
- 如 `addEventListener` 绑定事件
发布订阅模式
- Publisher 和 Observer 相互不认识,中间有媒介
- 如 `event` 自定义事件
一个很明显的特点:发布订阅模式需要在代码中触发 `emit` ,而观察者模式没有 `emit`
### 场景
#### 自定义事件
Vue2 实例本身就支持[自定义事件](https://cn.vuejs.org/v2/api/#vm-on),但 Vue3 不再支持。
Vue3 推荐使用 mitt ,轻量级 200 bytes ,文档 https://github.com/developit/mitt
```ts
import mitt from 'mitt'
const emitter = mitt() // 工厂函数
emitter.on('change', () => {
console.log('change1')
})
emitter.on('change', () => {
console.log('change2')
})
emitter.emit('change')
```
但是,mitt 没有 once ,需要可以使用 event-emitter https://www.npmjs.com/package/event-emitter
```ts
import eventEmitter from 'event-emitter' // 还要安装 @types/event-emitter
const emitter = eventEmitter()
emitter.on('change', (value: string) => {
console.log('change1', value)
})
emitter.on('change', (value: string) => {
console.log('change2', value)
})
emitter.once('change', (value: string) => {
console.log('change3', value)
})
emitter.emit('change', '张三')
emitter.emit('change', '李四')
```
#### postMessage 通讯
通过 `window.postMessage` 发送消息。注意第二个参数,可以限制域名,如发送敏感信息,要限制域名。
```js
// 父页面向 iframe 发送消息
window.iframe1.contentWindow.postMessage('hello', '*')
// iframe 向父页面发送消息
window.parent.postMessage('world', '*')
```
可监听 `message` 来接收消息。可使用 `event.origin` 来判断信息来源是否合法,可选择不接受。
```js
window.addEventListener('message', event => {
console.log('origin', event.origin) // 通过 origin 判断是否来源合法
console.log('child received', event.data)
})
```
同类型的还有
- nodejs 多进程通讯
- WebWorker 通讯
- WebSocket 通讯
### 注意事项
在 Vue 和 React 组件中使用,在组件销毁之前,要及时 off 自定义事件。否则可能会导致**内存泄漏**。
另,off 时要传入原来的函数,而不能是匿名函数。
### 总结
- 观察者模式 vs 发布订阅模式
- 发布订阅模式的场景:自定义事件
- 注意事项:及时 off
## 总结
(图文)
### 内容回归
- 概念介绍 + 解决的问题
- UML 类图和代码演示
- 场景
- 观察者模式 vs 发布订阅模式
### 重要细节
- Vue3 本身没有了自定义事件功能
- 组件销毁时及时 off 事件,防止内存泄漏
### 注意事项
- 观察者模式很重要,本章内容较多,耐心学习
- 观察者模式场景很多,要抓住重点,不要拘泥细节
另,观察者模式和发布订阅模式,本课程学习时区分明显,但在实际工作中并不会严格区分(减少沟通成本)
## 作业
### 题目
模拟面试:请描述观察者模式和发布订阅模式的区别。
### 提示
千万不要眼高手低,知道了就写出来试试。如果写起来磕磕绊绊,那你面试时也会磕磕绊绊。
另外,光文字描述还不够,还要配合图示、代码和使用场景。
# 04-迭代器模式
## 开始
(图文)
### 主要内容
- 概念介绍 + 解决的问题
- UML 类图和代码演示
- 场景
### 学习方法
- 结合使用场景学习
- 代码要结合 UML 类图
### 注意事项
- 前面可能听不懂,后面会豁然开朗,耐心学完
## 介绍
用于顺序访问集合对象的元素,不需要知道集合对象的底层表示。
### for 循环不是迭代器模式
简单的 for 循环并不是迭代器模式,因为 for 循环需要知道对象的内部结构。
如下面的例子
- 要知道数组的长度
- 要知道通过 `arr[i]` 形式来得到 item
```js
const arr = [10, 20, 30]
const length = arr.length
for (let i = 0; i < length; i++) {
console.log(arr[i])
}
```
### 简易迭代器
有些对象,并不知道他的内部结构
- 不知道长度
- 不知道如何获取 item
```js
const pList = document.querySelectorAll('p')
pList.forEach(p => console.log(p))
```
forEach 就是最建议的迭代器
## 演示
注意,这个示例也许你会感觉繁琐,不理解。但慢慢的把这一章看完,你就能明白她的意义。

```ts
class DataIterator {
private data: number[]
private index = 0
constructor(container: DataContainer) {
this.data = container.data
}
next(): number | null {
if (this.hasNext()) {
return this.data[this.index++]
}
return null
}
hasNext() {
if (this.index >= this.data.length) return false
return true
}
}
class DataContainer {
data: number[] = [10, 20, 30, 40]
getIterator() {
return new DataIterator(this)
}
}
const container = new DataContainer()
const iterator = container.getIterator()
while(iterator.hasNext()) {
const num = iterator.next()
console.log(num)
}
```
### 是否符合设计原则?
5 大设计原则中,最重要的就是:**开放封闭原则**,对扩展开放,对修改封闭
- 使用者和目标数据分离,解耦
- 目标数据自行控制内部迭代逻辑
- 使用者不关心目标数据的内部结构
## 场景
JS 有序对象,都内置迭代器
- 字符串
- 数组
- NodeList 等 DOM 集合
- Map
- Set
- arguments
【注意】对象 object 不是有序结构
### Symbol.iterator
每个有序对象,都内置了 `Symbol.iterator` 属性,属性值是一个函数。
执行该函数讲返回 iterator 迭代器,有 `next()` 方法,执行返回 `{ value, done }` 结构。
```js
// 拿数组举例,其他类型也一样
const arr = [10, 20, 30]
const iterator = arr[Symbol.iterator]()
iterator.next() // {value: 10, done: false}
iterator.next() // {value: 20, done: false}
iterator.next() // {value: 30, done: false}
iterator.next() // {value: undefined, done: true}
```
另外,有些对象的 API 也会生成有序对象
```js
const map = new Map([ ['k1', 'v1'], ['k2', 'v2'] ])
const mapIterator = map[Symbol.iterator]()
const values = map.values() // 并不是 Array
const valuesIterator = values[Symbol.iterator]()
// 还有 keys entries
```
### 自定义迭代器
```ts
interface IteratorRes {
value: number | undefined
done: boolean
}
class CustomIterator {
private length = 3
private index = 0
next(): IteratorRes {
this.index++
if (this.index <= this.length) {
return { value: this.index, done: false }
}
return { value: undefined, done: true }
}
[Symbol.iterator]() {
return this
}
}
const iterator = new CustomIterator()
console.log( iterator.next() )
console.log( iterator.next() )
console.log( iterator.next() )
console.log( iterator.next() )
```
### 有序结构的作用
#### for...of
所有有序结构,都支持 for...of 语法
#### 数组操作
数组解构
```js
const [node1, node2] = someDomList
```
扩展操作符
```js
const arr = [...someDomList]
```
Array.from()
```js
const arr = Array.form(someDomList)
```
#### 创建 Map 和 Set
```js
const map = new Map([
['k1', 'v1'],
['k2', 'v2']
])
const set = new Set(someDomList)
```
#### Promise.all 和 Promise.race
```js
Promise.all([promise1, promise2, promise3])
Promise.race([promise1, promise2, promise3])
```
#### yield* 操作符
下一节讲
### 总结
- 有序结构
- Symbol.iterator 生成迭代器
- 迭代器的应用
## Generator
迭代器和生成器,两者密不可分
### 基本使用
```js
function* genNums() {
yield 10
yield 20
yield 30
}
const numsIterator = genNums()
numsIterator.next() // {value: 10, done: false}
numsIterator.next() // {value: 20, done: false}
numsIterator.next() // {value: 30, done: false}
numsIterator.next() // {value: undefined, done: true}
// for (let n of numsIterator) {
// console.log(n)
// }
```
### yield* 语法
上一节说过,有序结构可用于 `yield*`
```js
function* genNums() {
yield* [100, 200, 300] // 相当于:循环数组,分别 yield
}
const numsIterator = genNums()
numsIterator.next() // {value: 100, done: false}
numsIterator.next() // {value: 200, done: false}
numsIterator.next() // {value: 300, done: false}
numsIterator.next() // {value: undefined, done: true}
// for (let n of numsIterator) {
// console.log(n)
// }
```
最简单的自定义迭代器
```js
class CustomIterator {
private data: number[]
constructor() {
this.data = [10, 20, 30]
}
* [Symbol.iterator]() {
yield* this.data
}
}
const iterator = new CustomIterator()
for (let n of iterator) {
console.log(n)
}
```
### yield 遍历 DOM 树
有助于深入理解 Generator
```js
function* traverse(elemList: Element[]): any {
for (const elem of elemList) {
yield elem
const children = Array.from(elem.children)
if (children.length) {
yield* traverse(children)
}
}
}
const container = document.getElementById('container')
if (container) {
for (let node of traverse([container])) {
console.log(node)
}
}
```
### 总结
- 基本使用
- yield* 语法
- 遍历 DOM 树
## 总结
(图文)
### 内容回顾
- 概念介绍 + 解决的问题
- UML 类图和代码演示
- 场景 Symbol.iterator 和 Generator
### 重要细节
- for...of 和迭代器的关系
- Generator 和迭代器的关系
- 对象 object 不是有序结构
### 注意事项
- 暂无
## 作业
### 题目
模拟面试:请使用 Generator 遍历一个 DOM 树
### 提示
如果感觉写起来有困难,先不要“直奔主题”。
可以先回顾一下 Generator 和 Iterator 的关系,然后回顾一下 `yield *` 语法。
再来结合深度优先遍历的过程,来思考这个问题。
# 05-原型模式
## 开始
(图文)
### 主要内容
- 介绍和演示
- JS 原型和原型链
- JS 属性描述符
### 学习方法
- 结合使用场景学习
- UML 类图
- 原型链的图亲自画一遍
### 注意事项
- 原型模式不常用,但原型和原型链是 JS 的基础
## 介绍
定义:用一个已经创建的实例作为原型,通过复制该原型对象来创建一个和原型相同或相似的新对象
传统的原型模式就是克隆,但这在 JS 中并不常用。

```ts
class CloneDemo {
name: string = 'clone demo'
clone(): CloneDemo {
return new CloneDemo()
}
}
```
JS 中并不常用原型模式,但 JS 对象本身就是基于原型的,原型和原型链是非常重要的概念。
## 原型和原型链
(图文小节)
### 函数和显示原型 `prototype`
JS 中所有函数都有一个 `prototype` 属性。例如
- `Object.prototype`
- `Array.prototype`
自定义的函数也有
```ts
// 1. 注意第一参数 this ;2. 暂且用 any 表示,实际会用 class
function Foo(this: any, name: string, age: number) {
this.name = name
this.age = age
}
Foo.prototype.getName = function () {
return this.name
}
Foo.prototype.sayHi = function () {
alert('hi')
}
```
### 对象和隐式原型 `__proto__`
#### 引用类型
JS 所有的引用类型对象都是通过函数创建的,都有 `__proto__` ,**指向其构造函数的 `prototype`**

```js
const obj = {} // 相当于 new Object()
obj.__proto__ === Object.prototype
const arr = [] // 相当于 new Array()
arr.__proto__ === Array.prototype
const f1 = new Foo('张三', 20)
f1.__proto__ === Foo.prototype
const f2 = new Foo('李四', 21)
f2.__proto__ === Foo.prototype
```
访问对象属性或 API 时,首先查找自身属性,然后查找它的 `__proto__`
```js
f1.name
f1.getName()
```
#### 值类型的 API
值类型没有 `__proto__` ,但它依然可访问 API 。因为 JS 会先将它包装为引用类型,然后触发 API
```js
const str = 'abc'
str.slice(0, 1) // 调用 String.prototype.string
```
### 原型链
上文讲过,一个对象的 `__proto__` 指向它构造函数的 `prototype` ,**而 `prototype` 本身也是一个对象,也会指向它构造函数的 `prototype`** ,于是就形成了原型链。

### class 是函数的语法糖
class 和函数一样,也是基于原型实现的。
```ts
class Foo {
name: string
age: number
constructor(name: string, age: number) {
this.name = name
this.age = age
}
getName() {
return this.name
}
sayHi() {
alert('hi')
}
}
Foo.prototype
const f1 = new Foo('张三', 20)
f1.__proto__ = Foo.prototype
```
### 继承
```ts
class People {
name: string
age: number
constructor(name: string, age: number) {
this.name = name
this.age = age
}
eat() {
alert(`${this.name} eat something`)
}
speak() {
alert(`My name is ${this.name}, age ${this.age}`)
}
}
class Student extends People {
school: string
constructor(name: string, age: number, school: string) {
super(name, age)
this.school = school
}
study() {
alert(`${this.name} study`)
}
}
const s1 = new Student('aa', 20, 'xx')
s1.study()
s1.eat()
```

### 总结
- 函数或 class 都有 prototype
- 对象都有 `__proto__` 指向构造函数 prototype
- 原型链(图)
PS:原型和原型链的范围非常广泛,遇到新问题可以在课程里提问。
## 场景
最符合原型模式的应用场景就是 `Object.create` ,它可以指定原型。
### 演示
```js
const obj1 = {}
obj1.__proto__
const obj2 = Object.create({x: 100})
obj2.__proto__
```
## JS 对象属性描述符
用于描述对象属性的一些特性
### 获取属性描述符
```ts
const obj = { x: 100 }
Object.getOwnPropertyDescriptor(obj, 'x')
// Object.getOwnPropertyDescriptors(obj)
```
### 设置属性描述符
```ts
Object.defineProperty(obj, 'y', {
value: 200,
writable: false,
// 其他...
// PS: 还可以定义 get set
})
```
使用 `Object.defineProperty` 定义新属性,属性描述符会默认为 false `{ configurable: false, enumerable: false, writable: false }`
而用 `{ x: 100 }` 字面量形式定义属性,属性描述符默认为 true
### 解释各个描述符
#### value
属性值:值类型、引用类型、函数等
```js
const obj = { x: 100 }
Object.defineProperty(obj, 'x', {
value: 101,
})
```
如果没有 value ,则打印 obj 就看不到属性。
```js
const obj = {}
let x = 100
Object.defineProperty(obj, 'x', {
get() {
return x
},
set(newValue) {
x = newValue
}
})
// console.log(obj)
// console.log(obj.x)
```
#### configurable
- 是否可以通过 delete 删除并重新定义
- 是否可以修改其他属性描述符配置
- 是否可以修改 get set
```js
const obj = { x: 100 }
Object.defineProperty(obj, 'y', {
value: 200,
configurable: false, // false
})
Object.defineProperty(obj, 'z', {
value: 300,
configurable: true,
})
delete obj.y // 不成功
// 重修修改 y 报错(而修改 z 就不报错)
Object.defineProperty(obj, 'y', {
value: 210
})
```
#### writable
属性是否可以被修改
```js
const obj = { x: 100 }
Object.defineProperty(obj, 'x', {
writable: false,
})
obj.x = 101
obj.x // 依然是 10
```
`Object.freeze()` 冻结对象:1. 现有属性值不可修改;2. 不可添加新属性;
```js
const obj = { x: 100, y: 200 }
Object.freeze(obj) // 冻结属性
obj.x = 101
obj.x // 100
// Object.getOwnPropertyDescriptor(obj, 'x') // { configurable: false, writable: false }
// obj.z = 300 // 不成功。不能再添加新属性
Object.isFrozen(obj) // true
```
PS:在 Vue 中,如果 data 中有比较大的对象,且不需要响应式,则可以使用 `Object.freeze()` 冻结。
对比 `Object.seal()` 密封对象:1. 现有属性值**可以修改**;2. 不可添加新属性;
```js
const obj = { x: 100, y: 200 }
Object.seal(obj)
Object.getOwnPropertyDescriptor(obj, 'x') // { configurable: false }
obj.x = 101 // 成功
// obj.z = 300 // 不成功。不能再添加新属性
Object.isSealed(obj) // true
```
PS:`Object.freeze()` 和 `Object.seal()` 是浅操作,不会递归下级属性
#### enumerable
是否可以通过 `for...in` 遍历到
```js
const obj = { x: 100 }
Object.defineProperty(obj, 'y', {
value: 200,
enumerable: false, // false
})
Object.defineProperty(obj, 'z', {
value: 300,
enumerable: true,
})
for (const key in obj) {
console.log(key) // 'x' 'z'
}
// console.log('y' in obj) // true —— 只能限制 for...in 无法限制 in
```
PS:对比 `for...in` 回顾 `for...of`
### 原型的属性描述符
在 N 年之前,使用 `for...in` 遍历对象时,需要用 `hasOwnProperty` 剔出原型属性,否则会把原型属性过滤出来。
```js
const obj = { x: 100 }
for (const key in obj) {
if (obj.hasOwnProperty(key)) {
console.log(key)
}
}
```
现在不用了,都是通过 `enumerable` 来判断
```js
Object.getOwnPropertyDescriptor(obj.__proto__, 'toString')
```
如果修改原型属性的 `enumerable` ,也是可以通过 `for...in` 遍历出来的
```js
const obj = { x: 100 }
Object.defineProperty(obj.__proto__, 'toString', { // 提醒,这里用于教学,实际项目不要修改原型属性的描述符!
enumerable: true
})
for (const key in obj) { console.log(key) }
// obj.hasOwnProperty('toString') // 依然是 false ,和 enumerable 没关系
```
还有,有些地方会修改函数的 prototype ,但却忽略了 `constructor` 的属性描述符。
```js
function Foo () {}
Foo.prototype = {
constructor: Foo, // 需要设置 { enumerable: false } ,否则它的实例 for...in 会有 constructor
fn1() {},
fn2() {}
}
```
### Symbol 类型
Object 的 symbol 属性,即便 `enumerable: true` 也无法通过 `for...in` 遍历
```js
const b = Symbol('b')
const obj = { a: 100, [b]: 200 }
for (const key in obj) { console.log(key) }
// Object.getOwnPropertyDescriptor(obj, b) // enumerable: true
```
获取 Symbol 属性,可使用 `getOwnPropertySymbols` 或 `Reflect.ownKeys`
```js
Object.keys(obj) // ['a']
Object.getOwnPropertySymbols(obj) // [ b ]
Reflect.ownKeys(obj) // ['a', b]
```
### 总结
- 获取属性描述符 Object.getOwnPropertyDescriptor
- 设置属性描述符 Object.defineProperty
- 各个属性描述符的作用
- enumerable 和 for...in
- for...in 和 for...of
- enumerable 和原型属性
## 总结
(图文)
### 内容回顾
- 介绍和演示
- JS 原型和原型链
- JS 属性描述符
### 重要细节
- 原型和原型链(图)
- 属性描述符 writable 和 Object.freeze()
- 属性描述符 enumerable 和 for...in
### 注意事项
- 原型模式不常用,但原型和原型链是 JS 的基础
## 作业
### 题目
对象属性描述符有几种,分别有什么作用,请通过代码演示来说明。
### 提示
不要眼高手低,感觉学会了的东西,就写出来试试。如果写的不顺利,那就说明学习的并不熟练。
不用写的那么详细,把每个属性描述符的核心价值写出来即可,但要配合代码演示。
# 06-装饰器模式
## 开始
(图文)
### 主要内容
- 概念介绍 + 解决的问题
- 代码演示和 UML 类图
- 使用场景:ES Decorator 和 AOP
### 学习方法
- 结合场景学习
- 代码结合 UML 类图
### 注意事项
- Angular 只为演示,国内应用不多
- AOP 先了解概念,不急于详细掌握(需要长时间才能理解,如 OOP )
## 介绍
装饰器模式(Decorator Pattern)允许向一个现有的对象添加新的功能,同时又不改变其结构。这种类型的设计模式属于结构型模式,它是作为现有的类的一个包装。动态地给一个对象添加一些额外的职责。就增加功能来说,装饰器模式相比生成子类更为灵活。
例如,手机上套一个壳可以保护手机,壳上粘一个指环,可以挂在手指上不容易滑落,这就是一种装饰。手机还是那个手机,手机的功能一点都没变,只是在手机的外面装饰了一些其他附加的功能。日常生活中,这样的例子非常多。

```ts
function decorate(phone) {
phone.fn3 = function () {
console.log('指环')
}
}
const phone = {
name: 'iphone12',
fn1() {}
fn2() {}
}
const newPhone = decorate(phone)
```
而 ES 语法允许我们这样写(其实就是语法糖),后面会详细讲
```ts
// 伪代码,不能运行
@decorate
const phone = { ... }
```
## 演示

```ts
class Circle {
draw() {
console.log('画一个圆')
}
}
class Decorator {
private circle: Circle
constructor(circle: Circle) {
this.circle = circle
}
draw() {
this.circle.draw()
this.setBorder()
}
private setBorder() {
console.log('设置边框颜色')
}
}
const circle = new Circle()
circle.draw()
const decorator = new Decorator(circle)
decorator.draw()
```
### 是否符合设计原则?
5 大设计原则中,最重要的就是:**开放封闭原则**,对扩展开放,对修改封闭
- 装饰器和目标分离,解耦
- 装饰器可自行扩展
- 目标也可自行扩展
## 场景
ES 引入了 Decorator 语法,TS 也支持
PS:在 tsconfig.json 中加 `experimentalDecorators: true`
### 装饰 class
```ts
// 装饰器
function testable(target: any) {
target.isTestable = true
}
@testable
class Foo {
static isTestable?: boolean
}
console.log(Foo.isTestable) // true
```
可以传入参数
```ts
// 装饰器工厂函数
function testable(val: boolean) {
// 装饰器
return function (target: any) {
target.isTestable = val
}
}
@testable(false)
class Foo {
static isTestable?: boolean
}
console.log(Foo.isTestable) // false
```
### 装饰 class 方法
```ts
function readOnly(target: any, key: string, descriptor: PropertyDescriptor) {
// console.log('target', target)
// console.log('key', key)
descriptor.writable = false
}
function configurable(val: boolean) {
return function (target: any, key: string, descriptor: PropertyDescriptor) {
descriptor.configurable = val
}
}
class Foo {
private _name = '张三'
private _age = 20
@readOnly
getName() {
return this._name
}
@configurable(false)
getAge() {
return this._age
}
}
const f = new Foo()
// f.getName = () => { return 'hello' } // 会报错
console.log(f.getName())
// @ts-ignore
// console.log( Object.getOwnPropertyDescriptor(f.__proto__, 'getAge') ) // { configurable: false }
console.log(f.getAge)
```
PS:其实 TS 本身有 `readOnly` 语法,但这里就是一个演示。
### react-redux
react-redux 的基本使用如下。文档参考 https://www.redux.org.cn/docs/basics/UsageWithReact.html
```js
import { connect } from 'react-redux'
const VisibleTodoList = connect(
mapStateToProps,
mapDispatchToProps
)(TodoList)
export default VisibleTodoList
```
如果使用装饰器就是
```js
import { connect } from 'react-redux'
// 装饰器
@connect(mapStateToProps, mapDispatchToProps)
export default VisibleTodoList extends React.Component { }
```
### Angular 定义组件
文档 https://angular.io/start
```ts
import { Component, OnInit } from '@angular/core';
// 装饰器,定义 class 为组件
@Component({
selector: 'app-product-alerts',
templateUrl: './product-alerts.component.html',
styleUrls: ['./product-alerts.component.css']
})
export class ProductAlertsComponent implements OnInit {
constructor() { }
ngOnInit() {}
}
```
### 总结
- 装饰 class
- 装饰 class 方法
- react-redux 和 Angular
## AOP
### 介绍
AOP - Aspect Oriented Programming 面向切面编程
简单来说:业务和系统基础功能分离,用 Decorator 很合适

PS:AOP 和 OOP 并不冲突
### 实现 log
```ts
function log(target: any, key: string, descriptor: PropertyDescriptor) {
const oldValue = descriptor.value // fn1 函数
// 重新定义 fn1 函数
descriptor.value = function () {
console.log(`记录日志...`)
return oldValue.apply(this, arguments)
}
}
class Foo {
@log // 不影响业务功能的代码,只是加了一个 log 的“切面”
fn1() {
console.log('业务功能1')
}
}
const f = new Foo()
f.fn1()
```
## 总结
(图文)
### 内容回顾
- 概念介绍 + 解决的问题
- 代码演示和 UML 类图
- 使用场景:ES Decorator 和 AOP
### 重要细节
- tsconfig.json 中 `experimentalDecorators: true`
- 装饰器如何传递参数(装饰器工厂函数)
- 装饰器函数的第三个参数:属性描述符
### 注意事项
- Angular 只为演示,国内应用不多
- AOP 先了解概念,不急于详细掌握(需要长时间才能理解,如 OOP )
## 作业
### 题目
根据你的理解,写一个 AOP 的示例:执行一个函数时自动打印一条日志。
### 提示
不要照抄课程内容,要默写。要按照自己的理解去写,不要死记硬背。
不要求和课程代码完全一致,能使用装饰器模式、符合 AOP 思想即可。
# 07-代理模式
## 开始
(图文)
### 主要内容
- 概念介绍 + 解决了什么问题
- 代码演示和 UML 类图
- 使用场景和 Proxy 语法
### 学习方法
- 结合使用场景理解
- Proxy 代码手写一遍
### 注意事项
- 注意 Proxy 的几个坑(后面会讲到)
## 介绍
为其他对象提供一种代理以**控制**对这个对象的访问。在直接访问对象时带来的问题,比如说:要访问的对象在远程的机器上。在面向对象系统中,有些对象由于某些原因(比如对象创建开销很大,或者某些操作需要安全控制,或者需要进程外的访问),直接访问会给使用者或者系统结构带来很多麻烦,我们可以在访问此对象时加上一个对此对象的访问层。

例如,你通过房产中介买房子,中介就是一个代理。你接触到的是中介这个代理,而非真正的房主。
再例如,明星都有经纪人,某活动想请明星演出,需要对接经纪人。艺术家不方便谈钱,但可以和经纪人谈。经纪人就是一个代理。
## 演示

```ts
class RealImg {
fileName: string
constructor(fileName: string) {
this.fileName = fileName
this.loadFromDist()
}
display() {
console.log('display...', this.fileName)
}
private loadFromDist() {
console.log('loading...', this.fileName)
}
}
class ProxyImg {
readImg: RealImg
constructor(fileName: string) {
this.readImg = new RealImg(fileName)
}
display() {
this.readImg.display()
}
}
const proxImg = new ProxyImg('xxx.png') // 使用代理
proxImg.display()
```
### 是否符合设计原则?
5 大设计原则中,最重要的就是:**开放封闭原则**,对扩展开放,对修改封闭
- 代理和目标分离,解耦
- 代理可自行扩展逻辑
- 目标也可自行扩展逻辑
## 场景
代理模式在前端很常用
### DOM 事件代理
```html
```
### webpack devServer
第一,配置 webpack ,参考 https://webpack.docschina.org/configuration/dev-server/#devserverproxy
```js
// webpack.config.js
module.exports = {
// 其他配置...
devServer: {
proxy: {
'/api': 'http://localhost:8081',
},
},
};
```
第二,启动 nodejs 服务,监听 `8081` 端口
第三,借用 axios 发送请求
```ts
import axios from 'axios'
document.getElementById('btn1')?.addEventListener('click', () => {
axios.get('/api/info')
.then(res => {
console.log(res)
})
})
```
### nginx 反向代理
nginx 配置文件可参考 https://www.runoob.com/w3cnote/nginx-setup-intro.html
```nginx
server {
listen 8000;
location / {
proxy_pass http://localhost:8001;
}
location /api/ {
proxy_pass http://localhost:8002;
proxy_set_header Host $host;
}
}
```
反向代理 vs 正向代理
(视频里画图解释)
### Proxy
Vue3 就使用 Proxy 做 data 响应式
```ts
// 明星
const star = {
name: '张三',
age: 25,
phone: '18611112222',
price: 0 // 艺术物价,明星不谈钱
}
// 经纪人
const agent = new Proxy(star, {
get(target, key) {
if (key === 'phone') {
return '13900001111' // 返回经纪人的的电话
}
if (key === 'price') {
return 100 * 1000 // 报价
}
return Reflect.get(target, key) // 返回原来的属性值
},
set(target, key, val): boolean {
if (key === 'price') {
if (val < 100 * 1000) {
throw new Error('价格太低了...')
} else {
console.log('报价成功,合作愉快!', val)
return Reflect.set(target, key, val)
}
}
// 其他属性不可设置
return false
}
})
// 主办方
console.log(agent.name)
console.log(agent.age)
console.log(agent.phone)
console.log(agent.price)
// agent.price = 90000 // 价格低了会报错
```
### 总结
- DOM 事件代理
- webpack-dev-server 代理
- nginx 反向代理
- Proxy
## Proxy 的使用场景
### 跟踪属性访问
Vue3 就是通过这个特性实现数据响应式
```ts
const user = {
name: '张三'
}
const proxy = new Proxy(user, {
get(target, key) {
console.log('get...')
return Reflect.get(target, key)
},
// get(...args) {
// return Reflect.get(...args)
// },
set(target, key, val) {
console.log('set...', val)
return Reflect.set(target, key, val)
}
})
proxy.name = '李四'
console.log(proxy.name)
```
### 隐藏属性
```ts
const hiddenProps = ['girlfriend'] // 要隐藏的属性 key
const user = {
name: '张三',
age: 25,
girlfriend: '小红'
}
const proxy = new Proxy(user, {
get(target, key) {
if (hiddenProps.includes(key as string)) return undefined
return Reflect.get(target, key)
},
has(target, key) {
if (hiddenProps.includes(key as string)) return false
return Reflect.has(target, key)
},
set(target, key, val) {
if (hiddenProps.includes(key as string)) return false
console.log('set...', val)
return Reflect.set(target, key, val)
}
})
console.log('age', proxy.age)
console.log('girlfriend', proxy.girlfriend) // undefined
```
### 验证属性
如果用 TS ,会有静态类型检查,用不到这个验证。用 JS 的话会有效果。
以下代码可以在浏览器中运行(非 TS 环境)
```ts
const user = {
name: '张三',
age: 25,
}
const proxy = new Proxy(user, {
get(target, key) {
return Reflect.get(target, key)
},
set(target, key, val) {
if (key === 'age') {
if (typeof val !== 'number') return false // 验证 age 类型
}
return Reflect.set(target, key, val)
}
})
proxy.age = 'a'
console.log(proxy.age) // 25
```
### 记录实例
```ts
const userList = new WeakSet() // 每次初始化 user ,都记录到这里
class User {
name: string
constructor(name: string) {
this.name = name
}
}
const ProxyUser = new Proxy(User, {
construct(...args) {
const user = Reflect.construct(...args)
userList.add(user) // 记录 user 对象
return user
}
})
const user1 = new ProxyUser('张三')
const user2 = new ProxyUser('李四')
console.log('userList', userList)
```
### 总结
- 跟踪属性访问 get set
- 隐藏属性
- 验证属性
- 记录实例
## Proxy 的注意事项
### 捕获器不变式
这是“红宝书”里的叫法。捕获器即 get ,不变式即不能因为 Proxy 而改变对象本身的描述符特性。
```ts
const obj = { x: 100, y: 0 }
Object.defineProperty(obj, 'y', {
value: 200,
writable: false,
configurable: false,
})
const proxy = new Proxy(obj, {
get() {
return 'abc'
}
})
console.log(proxy.x)
console.log(proxy.y) // y 属性描述符被修改,proxy 不能修改它的值
```
### this
函数里的 this 是由执行时确认的,而非定义时。
```ts
const user = {
name: '张三',
getName() {
console.log('this...', this)
return this.name
}
}
const proxy = new Proxy(user, {})
user.getName() // 执行时 this 是 user
proxy.getName() // 执行时 this 是 proxy
```
### 总结
- 捕获器不变式
- this
## 总结
(图文)
### 内容回顾
- 概念介绍 + 解决了什么问题
- 代码演示和 UML 类图
- 使用场景和 Proxy 语法
### 重要细节
- Proxy 使用场景
- Proxy 注意事项
### 注意事项
暂无
## 作业
### 题目
写代码,使用 Proxy 语法监听一个对象所有属性的 get 和 set 。
### 提示
不要照抄课程内容,要默写。要按照自己的理解去写,不要死记硬背。
不要求和课程代码完全一致,能实现功能即可。
# 08-其他模式
## 开始
(图文)
### 重要内容
- 职责链模式
- 策略模式
- 适配器模式
- MVC 和 MVVM
### 学习方法
- 结合场景和实战
### 注意事项
- 非常用设计模式,不要细扣概念,要领悟它对于实际开发的指导意义
- MVC 和 MVVM 要结合实战
## 职责链模式
顾名思义,就是一步操作可能分位多个职责角色来完成,把这些角色都分开,然后用一个链串起来。这样就将请求者和处理者、包括多个处理者之间进行了分离。
前端最常见的就是**链式操作**。
### jQuery 链式操作
```js
$('#div1')
.show()
.css('color', 'red')
.append($('#p1'))
```
### Promise 链式操作
```ts
// 加载图片
function loadImg(src: string) {
const promise = new Promise((resolve, reject) => {
const img = document.createElement('img')
img.onload = () => {
resolve(img)
}
img.onerror = () => {
reject('图片加载失败')
}
img.src = src
})
return promise
}
const src = 'https://www.imooc.com/static/img/index/logo_new.png'
const result = loadImg(src)
result.then((img: HTMLImageElement) => {
console.log('img.width', img.width)
return img
}).then((img: HTMLImageElement) => {
console.log('img.height', img.height)
}).catch((err) => {
console.log(err)
})
```
## 策略模式
主要解决多个 `if...else` 或者 `switch...case` 的问题。
把每种情况分成多种策略,分别实现。
```ts
class User {
private type: string
constructor(type: string) {
this.type = type
}
buy() {
const { type } = this
if (type === 'ordinary') {
console.log('普通用户购买')
}
if (type === 'member') {
console.log('会员购买')
}
if (type === 'vip') {
console.log('VIP 用户购买')
}
}
}
const u1 = new User('ordinary')
u1.buy()
const u2 = new User('member')
u2.buy()
const u3 = new User('vip')
u3.buy()
```
使用策略模式
```ts
interface IUser {
buy: () => void
}
class OrdinaryUser implements IUser {
buy() {
console.log('普通用户购买')
}
}
class MemberUser implements IUser {
buy() {
console.log('会员购买')
}
}
class VipUser implements IUser {
buy() {
console.log('VIP 用户购买')
}
}
const u1 = new OrdinaryUser()
u1.buy()
const u2 = new MemberUser()
u2.buy()
const u3 = new VipUser()
u3.buy()
```
## 适配器模式
### 介绍
我们需要一个对象的 API 提供能力,但它的格式不一定完全适合我们的格式要求。这就要转换一下。
例如电脑、手机的电源适配器

### 演示
```ts
// 电源插口
class Source {
supply() {
return '220V 电源'
}
}
// 适配器
class Adapter {
source = new Source()
adaptedSupply() {
const sourceRes = this.source.supply()
return `${sourceRes} --> 12V 电源`
}
}
// 手机使用
const adapter = new Adapter()
const res = adapter.adaptedSupply()
console.log(res)
```
### 场景
Vue computed
```js
// Vue 组件配置
{
data() {
return {
userList: [
{ id: 1, name: '张三' },
{ id: 2, name: '李四' },
{ id: 3, name: '王五' },
]
}
},
computed: {
userNameList() {
this.userList.map(user => user.name) // ['张三', '李四', ... ]
}
}
}
```
## MVC 和 MVVM
MVC 和 MVVM 不属于经典的 23 种设计模式,但也可以说他们是设计模式。
本来设计模式就是一种抽象的定义,而且随着时代的发展,它也需要慢慢的改变。
如何称呼无所谓,关键是理解它们的内容。
### MVC
MVC 原理
- View 传送指令到 Controller
- Controller 完成业务逻辑后,要求 Model 改变状态
- Model 将新的数据发送到 View,用户得到反馈

### MVVM
MVVM 直接对标 Vue 即可
- View 即 Vue template
- Model 即 Vue data
- VM 即 Vue 其他核心功能,负责 View 和 Model 通讯


### 总结
- MVC
- MVVM
PS:先了解概念,再去实战,先“文”而后“化”
## 总结
(图文)
### 重要内容
- 职责链模式
- 策略模式
- 适配器模式
- MVC 和 MVVM
### 注意事项
- 非常用设计模式,不要细扣概念,要领悟它对于实际开发的指导意义
- MVC 和 MVVM 要结合实战
## 作业
### 题目
请分别描述职责链模式、策略模式、适配器模式的作用和场景。
### 提示
这三个设计模式虽然并不直接使用,但它们却体现了非常好的编程思想,这是常用的。
亲自写出来才能记忆更加牢固。
## 实战演练
(图文)
### 主要内容
- 面试题 - 模拟打车
- 面试题 - 模拟停车场
### 学习方法
- 详细审题,抓住细节和重点
- 分析:抽象数据模型,梳理关系,定义属性和方法
- 最后再 UML 类图,写代码
### 注意事项
- 先审题、分析,切忌一开始就写代码
- 关注整体设计,别较真细节
## 面试题 - 打车
### 题目
背景
- 打车时,你可以打快车和专车
- 无论什么车,都有车牌号和车辆名称
- 打不同的车价格不同,快车每公里 1 元,专车每公里 2 元
- 打车时,你要启动行程并显示车辆信息
- 结束行程,显示价格(假定行驶了 5 公里)
题目
- 画出 UML 类图
- 用 TS 语法写出该示例
### 分析
抽象数据模型
- 车,抽象为一个 class
- 快车,专车是派生类,有一个父类或接口 (快车、专车,有相同,有不同)
- 行程,抽象为一个 class ,且和车有关系
属性和方法
- 车:车牌号,名称(在父类或接口),价格(在派生类)
- 行程:开始,结束
- 行程:关联的车辆
### UML 类图

### 代码演示
```ts
class Car {
name: string
number: string
price = 0
constructor(name: string, number: string) {
this.name = name
this.number = number
}
}
class ExpressCar extends Car {
price = 1
constructor(name: string, number: string) {
super(name, number)
}
}
class SpecialCar extends Car {
price = 2
constructor(name: string, number: string) {
super(name, number)
}
}
class Trip {
car: Car
constructor(car: Car) {
this.car = car
}
start() {
console.log(`行程开始,名称: ${this.car.name}, 车牌号: ${this.car.number}`)
}
end() {
console.log('行程结束,价格: ' + (this.car.price * 5))
}
}
// const car = new ExpressCar('桑塔纳', 'A111222')
const car = new SpecialCar('迈腾', 'B333444')
const trip = new Trip(car)
trip.start()
trip.end()
```
### 总结
- 分析:数据模型,属性和方法,关系
- UML 类图
- 代码演示
## 面试题 - 停车场
### 题目
描述:
- 某停车场,分 3 层,每层 100 车位
- 每个车位可以监控车辆的进入和离开
- 车辆进入前,显示每层的空余车位数量
- 车辆进入时,摄像头可识别车牌号和时间
- 车辆出来时,出口显示器显示车牌号和停车时长
题目:
- 画出 UML 类图
(代码量较多,正式面试时不用一行一行写,画图即可)
### 分析
数据模型
- 车
- 停车场,层,车位
- 摄像头,显示屏
梳理流程
- 进入之前:显示当前空余车位
- 进入:计停车数量,计算开始时间
- 离开:计算结束时间,减停车数量
### UML 类图

### 代码演示
HTML 代码
```html
```
TS 代码
```ts
// 车
class Car {
number: string
constructor(number: string) {
this.number = number
}
}
// 停车信息
interface IEntryInfo {
number: string
inTime: number
place?: ParkPlace
}
// 入口摄像头
class ParkCamera {
// 拍照
shot(car: Car): IEntryInfo {
return {
number: car.number,
inTime: Date.now()
}
}
}
// 出口显示器
class ParkScreen {
show(info: IEntryInfo) {
const { inTime, number } = info
const duration = Date.now() - inTime
console.log(`车牌号:${number} ,停留时间:${duration}`)
}
}
// 车位
class ParkPlace {
isEmpty = true
getInto() {
this.isEmpty = false
}
out() {
this.isEmpty = true
}
}
// 层
class ParkFloor {
index: number
parkPlaces: ParkPlace[]
constructor(index: number, places: ParkPlace[]) {
this.index = index
this.parkPlaces = places
}
get emptyPlaceNum(): number {
let num = 0
for (const place of this.parkPlaces) {
if (place.isEmpty) num++
}
return num
}
}
// 停车场
class Park {
parkFloors: ParkFloor[]
parkCamera = new ParkCamera()
parkScreen = new ParkScreen()
entryInfoList: Map = new Map() // key 是 car.number
constructor(floors: ParkFloor[]) {
this.parkFloors = floors
}
getInto(car: Car) {
// 获取摄像头的信息:车牌号,时间
const entryInfo = this.parkCamera.shot(car)
// 某个车位
const i = Math.round((Math.random() * 100) % 100)
const place = this.parkFloors[0].parkPlaces[i] // 停在第一层的某个车位(想要第二层,第三层,也可以用随机数获取)
// 进入车位
place.getInto()
// 记录停车信息
entryInfo.place = place
this.entryInfoList.set(car.number, entryInfo)
}
out(car: Car) {
// 获取停车信息
const entryInfo = this.entryInfoList.get(car.number)
if (entryInfo == null) return
const { place } = entryInfo
if (place == null) return
// 从车位离开
place.out()
// 出口显示屏,显示
this.parkScreen.show(entryInfo)
// 删除停车信息
this.entryInfoList.delete(car.number)
}
// 当前停车场的空余车位
get emptyInfo(): string {
return this.parkFloors.map(floor => {
return `${floor.index} 层还有 ${floor.emptyPlaceNum} 个车位`
}).join('\n')
}
}
// ---------- 初始化停车场 ----------
const floors: ParkFloor[] = []
// 3 层
for (let i = 0; i < 3; i++) {
const places: ParkPlace[] = []
// 每层 100 个车位
for (let j = 0; j < 100; j++) {
places[j] = new ParkPlace()
}
floors[i] = new ParkFloor(i + 1, places)
}
const park = new Park(floors)
// ---------- 模拟车辆进入、离开 ----------
const car1 = new Car('A1')
const car2 = new Car('A2')
const car3 = new Car('A3')
document.getElementById('btn-car1-into')?.addEventListener('click', () => {
console.log('第一辆车即将进入')
console.log(park.emptyInfo)
park.getInto(car1)
})
document.getElementById('btn-car1-out')?.addEventListener('click', () => {
console.log('第一辆车离开')
park.out(car1)
})
document.getElementById('btn-car2-into')?.addEventListener('click', () => {
console.log('第二辆车即将进入')
console.log(park.emptyInfo)
park.getInto(car2)
})
document.getElementById('btn-car2-out')?.addEventListener('click', () => {
console.log('第二辆车离开')
park.out(car2)
})
document.getElementById('btn-car3-into')?.addEventListener('click', () => {
console.log('第三辆车即将进入')
console.log(park.emptyInfo)
park.getInto(car3)
})
document.getElementById('btn-car3-out')?.addEventListener('click', () => {
console.log('第三辆车离开')
park.out(car3)
})
```
### 总结
- 题目信息
- 分析
- UML 类图
- 代码演示
## 总结
(图文)
### 内容回顾
- 面试题 - 模拟打车
- 面试题 - 模拟停车场
### 注意事项
- 先审题、分析,切忌一开始就写代码
- 关注整体设计,别较真细节
## 作业
### 题目
如果面试遇到设计相关的问题,你应该如何思考,分几个步骤?
### 提示
思考方式很重要,课程里也重点强调过了。
如果思考方式不对,将会事倍功半,特别是在面试这样一个时间紧张的环境下。
所以,出一个作业题目,让大家重点记忆,刻意练习。
### 提交
可以把作业内容提交到课程提问区,讲师会定期评审答复。