# 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) ``` ### 注意 工厂模式可以拆分为三个: - 工厂方法模式 - 抽象工厂模式 - 建造者模式 前端用不到这么细致,只需要掌握核心的工厂模式即可。 ## 演示 ### 标准的工厂模式 ![](./img/工厂模式1.png) ```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') } } ``` ### 简单的工厂模式 ![](./img/工厂模式2.png) ```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) // 获取不到 ``` ## 演示 ![](./img/单例模式.png) ### 使用 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 的生命周期,也是观察者模式 ![](./img/vue-生命周期.png) ## 演示 Subject 和 Observer 是**一对多**的关系 ![](./img/观察者模式.png) ```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),负责创建,也得复杂销毁。 ![](./img/vue-生命周期.png) ### Vue watch ```js // Vue 组件配置 { data() { name: '双越' }, watch: { name(newVal, val) { console.log(newValue, val) } } } ``` PS:面试题 watch 和 watchEffect 有什么区别?—— 请看我的面试课程 ### Vue 组件更新过程 PS:React 组件更新过程不是这样的,它是通过 `setState` 主动触发的,而非响应式监听。 ![](./img/vue-组件更新过程.png) ### 各种异步的回调 #### 定时器 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

A

B

``` 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 发布订阅模式 ![](./img/观察者vs发布订阅.png) 观察者模式 - 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 就是最建议的迭代器 ## 演示 注意,这个示例也许你会感觉繁琐,不理解。但慢慢的把这一章看完,你就能明白她的意义。 ![](./img/迭代器模式.png) ```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 中并不常用。 ![](./img/原型模式.png) ```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`** ![](./img/原型.png) ```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`** ,于是就形成了原型链。 ![](./img/原型链.png) ### 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() ``` ![](./img/原型链2.png) ### 总结 - 函数或 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)允许向一个现有的对象添加新的功能,同时又不改变其结构。这种类型的设计模式属于结构型模式,它是作为现有的类的一个包装。动态地给一个对象添加一些额外的职责。就增加功能来说,装饰器模式相比生成子类更为灵活。 例如,手机上套一个壳可以保护手机,壳上粘一个指环,可以挂在手指上不容易滑落,这就是一种装饰。手机还是那个手机,手机的功能一点都没变,只是在手机的外面装饰了一些其他附加的功能。日常生活中,这样的例子非常多。 ![](./img/装饰器模式1.png) ```ts function decorate(phone) { phone.fn3 = function () { console.log('指环') } } const phone = { name: 'iphone12', fn1() {} fn2() {} } const newPhone = decorate(phone) ``` 而 ES 语法允许我们这样写(其实就是语法糖),后面会详细讲 ```ts // 伪代码,不能运行 @decorate const phone = { ... } ``` ## 演示 ![](./img/装饰器模式.png) ```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 很合适 ![](./img/AOP.png) 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 的几个坑(后面会讲到) ## 介绍 为其他对象提供一种代理以**控制**对这个对象的访问。在直接访问对象时带来的问题,比如说:要访问的对象在远程的机器上。在面向对象系统中,有些对象由于某些原因(比如对象创建开销很大,或者某些操作需要安全控制,或者需要进程外的访问),直接访问会给使用者或者系统结构带来很多麻烦,我们可以在访问此对象时加上一个对此对象的访问层。 ![](./img/代理模式1.png) 例如,你通过房产中介买房子,中介就是一个代理。你接触到的是中介这个代理,而非真正的房主。 再例如,明星都有经纪人,某活动想请明星演出,需要对接经纪人。艺术家不方便谈钱,但可以和经纪人谈。经纪人就是一个代理。 ## 演示 ![](./img/代理模式.png) ```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
a1 a2 a3 a4
``` ### 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 提供能力,但它的格式不一定完全适合我们的格式要求。这就要转换一下。 例如电脑、手机的电源适配器 ![](./img/电源适配器.png) ### 演示 ```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,用户得到反馈 ![](./img/MVC.png) ### MVVM MVVM 直接对标 Vue 即可 - View 即 Vue template - Model 即 Vue data - VM 即 Vue 其他核心功能,负责 View 和 Model 通讯 ![](./img/MVVM.png) ![](./img/vue-mvvm.png) ### 总结 - MVC - MVVM PS:先了解概念,再去实战,先“文”而后“化” ## 总结 (图文) ### 重要内容 - 职责链模式 - 策略模式 - 适配器模式 - MVC 和 MVVM ### 注意事项 - 非常用设计模式,不要细扣概念,要领悟它对于实际开发的指导意义 - MVC 和 MVVM 要结合实战 ## 作业 ### 题目 请分别描述职责链模式、策略模式、适配器模式的作用和场景。 ### 提示 这三个设计模式虽然并不直接使用,但它们却体现了非常好的编程思想,这是常用的。 亲自写出来才能记忆更加牢固。 ## 实战演练 (图文) ### 主要内容 - 面试题 - 模拟打车 - 面试题 - 模拟停车场 ### 学习方法 - 详细审题,抓住细节和重点 - 分析:抽象数据模型,梳理关系,定义属性和方法 - 最后再 UML 类图,写代码 ### 注意事项 - 先审题、分析,切忌一开始就写代码 - 关注整体设计,别较真细节 ## 面试题 - 打车 ### 题目 背景 - 打车时,你可以打快车和专车 - 无论什么车,都有车牌号和车辆名称 - 打不同的车价格不同,快车每公里 1 元,专车每公里 2 元 - 打车时,你要启动行程并显示车辆信息 - 结束行程,显示价格(假定行驶了 5 公里) 题目 - 画出 UML 类图 - 用 TS 语法写出该示例 ### 分析 抽象数据模型 - 车,抽象为一个 class - 快车,专车是派生类,有一个父类或接口 (快车、专车,有相同,有不同) - 行程,抽象为一个 class ,且和车有关系 属性和方法 - 车:车牌号,名称(在父类或接口),价格(在派生类) - 行程:开始,结束 - 行程:关联的车辆 ### UML 类图 ![](./img/面试题-打车.png) ### 代码演示 ```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 类图 ![](./img/面试题-停车场.png) ### 代码演示 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 类图 - 代码演示 ## 总结 (图文) ### 内容回顾 - 面试题 - 模拟打车 - 面试题 - 模拟停车场 ### 注意事项 - 先审题、分析,切忌一开始就写代码 - 关注整体设计,别较真细节 ## 作业 ### 题目 如果面试遇到设计相关的问题,你应该如何思考,分几个步骤? ### 提示 思考方式很重要,课程里也重点强调过了。 如果思考方式不对,将会事倍功半,特别是在面试这样一个时间紧张的环境下。 所以,出一个作业题目,让大家重点记忆,刻意练习。 ### 提交 可以把作业内容提交到课程提问区,讲师会定期评审答复。