From c99005a4fc1df080af46a8425004abd5a328d8af Mon Sep 17 00:00:00 2001 From: dselegent Date: Fri, 3 Mar 2023 11:30:09 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20:sparkles:=20=E5=A2=9E=E5=8A=A0=2001=20?= =?UTF-8?q?=E3=80=90=E5=89=8D=E7=AB=AF=E5=B8=B8=E7=94=A8=E8=AE=BE=E8=AE=A1?= =?UTF-8?q?=E6=A8=A1=E5=BC=8F=E3=80=91.md?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...41\346\250\241\345\274\217\343\200\221.md" | 456 ++++++++++++++++++ 1 file changed, 456 insertions(+) create mode 100644 "\345\211\215\347\253\257\350\256\276\350\256\241\346\250\241\345\274\217/01 \343\200\220\345\211\215\347\253\257\345\270\270\347\224\250\350\256\276\350\256\241\346\250\241\345\274\217\343\200\221.md" diff --git "a/\345\211\215\347\253\257\350\256\276\350\256\241\346\250\241\345\274\217/01 \343\200\220\345\211\215\347\253\257\345\270\270\347\224\250\350\256\276\350\256\241\346\250\241\345\274\217\343\200\221.md" "b/\345\211\215\347\253\257\350\256\276\350\256\241\346\250\241\345\274\217/01 \343\200\220\345\211\215\347\253\257\345\270\270\347\224\250\350\256\276\350\256\241\346\250\241\345\274\217\343\200\221.md" new file mode 100644 index 0000000..261dded --- /dev/null +++ "b/\345\211\215\347\253\257\350\256\276\350\256\241\346\250\241\345\274\217/01 \343\200\220\345\211\215\347\253\257\345\270\270\347\224\250\350\256\276\350\256\241\346\250\241\345\274\217\343\200\221.md" @@ -0,0 +1,456 @@ +# 01 【前端常用设计模式】 + +## 1.何为设计模式?&&为什么要使用? + +历史就不谈了,大家可以百度~~简单点说,就是在不同业务情况下,要如何去解决问题的一种方案,让业务代码变得灵活,增强复用性,可维护性,增强业务代码面对不同场景的适应能力!可以说设计模式是我们初级前端走向中高级前端不能少走的一步! + +## 2.设计模式原则表(简) + +| 开闭原则 | 对拓展开放,对修改关闭 | +| ---------------- | ----------------------------- | +| **里氏替换原则** | **不要破坏继承体系** | +| **合成复用原则** | **少用继承,多用合成关系实现** | +| **依赖倒转原则** | **面向接口编程** | +| **迪米特法则** | **降低代码耦合度** | +| **单一职责原则** | **类的职责要单一** | +| **接口隔离原则** | **设计接口的时候要精简单一** | + +## 3.设计模式类型 + +设计模式也是细分了几种类型的,这几种类型之间区别也是有所不同,简单给大家介绍一下核心区别(标注了星号的为本文介绍的模式) + +### 3.1 创造型 + +**该模式处理的是用于创建对象的各种机制,这种模式着眼于优化的或更可控的对象创建机制;** + +**包含以下但不限于这几种模式:** + +- 工厂模式 +- 抽象工厂 +- 建造者 +- 原型 +- 单例模式(*) + +### 3.2 结构型 + +**这个类型的设计模所考虑的是对象的组成和对象之间的关系,假如对象发生了重大改变,对当前对象操作影响降至最低** + +包含以下但不限于这几种模式: + +- 适配器模式 +- 桥接模式 +- 装饰器模式 +- 外观模式 +- 享元模式 +- 代理模式 + +### 3.3 行为型 + +**该模式关注的是对象之间的依赖关系以及通信** + +包含以下但不限于这几种模式: + +- 解释器 +- 模板方法 +- 责任链 +- 命令模式(*) +- 迭代器 +- 中介者 +- 备忘录 +- 观察者模式(*) +- 状态 +- 策略模式(*) +- 访问者 + +## 4.单例模式 + +### 4.1 核心概念 + +单例模式的定义是,保证一个类仅有一个实例,并且要提供访问他的全局api + +> 单例模式在前端是一种很常见的模式,一些对象我们往往就只需要一个,如果你使用过VueX,React-redux等框架全局状态管理工具进行项目开发,你不难发现,这类工具库也是运用了单例模式的特性,用途相当广泛,要使用JavaScript实现一个标准的单例很简单,就是使用一个变量作为标识来判断当前是否已经创建过对象,如果没有就创建,如果已经创建则返回之前创建过的对象 + +基于核心概念确定单例模式功能 + +- 确保一个类只有一个实例 +- 提供全局访问的api。 + +### 4.2 简单实现代码(class语法风格) + +```js +class Singleton { + constructor (name) { + this.name = name + } + // 静态方法 + static getInstance (name) { + if (!this.instance) { + this.instance = new Singleton(name) + } + return this.instance + } +} +let a = Singleton.getInstance('a1') +let b = Singleton.getInstance('b2') +console.log(a == b) +``` + +### 4.3 简单代码实现(闭包函数风格) + +```js +const Singleton = function (name) { + this.name = name +} +// 利用自执行函数产生闭包 +Singleton.getInstance = (function () { + var instance + return function (name) { + if (!instance) { + instance = new Singleton(name) + } + return instance + } +})() +let a = Singleton.getInstance('a1') +let b = Singleton.getInstance('b2') +console.log(a===b) // true +``` + +### 4.4 JavaScript中的单例模式 + +> 前面的几种实现方式,他们更多接近的传统面向对象语言的实现,对于JavaScript这种无类语言来说有点穿棉衣洗澡,因为传统面向对象语言单例对象从"类"中创建而来,而我们天生拥有及简的对象创建方式,大可不必模仿强类型语言去实现单例,对没错!我们只需要直接创建对象就是单例模式,只要做好以下两点 + +- 保证创建的对象是唯一 +- 并且提供方法给全局使用 + +小伙伴可能猜到了,能提供给全局访问的是不是只有全局变量了,是的没错 + +```js +var a = {} +复制代码 +``` + +这段代码声明一个全局a对象,这时候的a确实是独一无二的,但是在大型项目中,多人参与项目开发需要单例特性,如果统统采用这种方式声明,那么必然会造成命名空间污染,JavaScript中的变量很容易被覆盖,JavaScript的作者都说全局变量是一个糟糕的特性,作为普通开发者的我们,我们其实有必要去减少全局变量污染问题! + +如何避免变量污染呢? + +**1.使用命名空间** + +```js +let MyApp = { + a:function(){ + console.log('a') + }, + b:function(){ + console.log('b') + } +} +``` + +能看到a变量已经减少和全局作用域打交道的机会 + +**2.使用闭包特性+命名空间实现变量私有化** + +```js +let MyApp2 = (function () { + let _name = 'sven', + _age = 18 + return { + getUserInfo () { + return _name + '-' + _age + } + } +})() + +MyApp2._name = 'sb' // 尝试修改 +console.log(MyApp2.getUserInfo()) // sven-18 没修改成功 +``` + +现在外界是真的访问不到这两个变量了,成功避免全局污染~~ + +### 4.5 惰性单例 + +惰性单例才是单例模式的重点!它所指的是,在需要的时候才创建实例对象;这模式在真实开发极其有用! + +我们来模拟一个场景,我们正在开发一个网站,网站类型是一个视频网站,网站有个登录按钮,点击登录会弹出一个登录框进行登录,你现在可能已经联想到,这个登录框一定是页面唯一的一个dom节点,一个页面存在两个登录框是不存在的! + +如果要实现这种效果第一种解决方案就是在页面加载的时候就已经创建好dom节点,并且设置样式为display为none,当点击登录时修改为block显示; + +这种解决方式有一个问题,我作为普通用户没vip我可能都不会去点这个登录,假如用户一点进来你就开始创建这个dom节点,那么可能就会浪费一些性能; + +```js +var Model = function () { + var div = document.createElement('div') + instance.className = 'modal' + div.innerHTML = '我是登录窗口' + div.style.display = 'none' + document.body.appendChild(div) + return div +} + +button.addEventListener('click', function () { + let div = createDiv() + div.style.display = 'block' +}) +``` + +这样我们达成了惰性的特征,及需要的时候才进行创建,但是失去了单例效果,频繁的创建删除dom节点也是不合理的地方!我们再结合之前学过的单例特性运用到Model函数上进行修改 + +```html + + + + +``` + +**class 语法** + +```js +class Model { + static createModel() { + if (!this.instance) { + this.instance = document.createElement('div') + this.instance.className = 'modal' + this.instance.innerText = '登录对话框' + document.body.append(this.instance) + } + return this.instance + } +} + +document.querySelector('#open').addEventListener('click', () => { + const modal = Model.createModel() + modal.style.display = 'block' +}) +document.querySelector('#close').addEventListener('click', () => { + const modal = Model.createModel() + modal.style.display = 'none' +}) +``` + +### 4.6 小结 + +单例模式可以说在前端范围你不可能遇不上,特别是单例惰性模式,在适合的时候才创建对象,并且只创建唯一的一个,如果创建对象和管理创建单例职责分布在两个不同的方法当中,解耦性的加持会让这个模式威力大大增加,这是能提高性能的一个突破口,Vue,React的路由懒加载的实现都是有着单例惰性思想在里边,把单例模式的一些想法学好你就能改动你现有大部分代码。 + +## 5.策略模式 + +定义一系列算法,把它们一个个封装起来,**并且使他们可以相互替换** + +> 注意**"并且使他们可以相互替换"**,这句话其实是不适用在JavaScript这种动态语言身上的,站在JavaScript角度理解这句话,要这么理解,定义一系列的算法,把他们封装不同的类,这些策略类都拥有相同的方法,算法被封在方法内部里,在开发者调用Context接口时,Context总是会把请求委托给这些封装好的策略类来完成需求 + +为了更好的讲解这个模式的应用我先引出一个需求并确定满足策略模式的功能 + +- 分离算法 +- 由Context调用委托给策略类 + +我现在是一个人,我身边有很多形形色色的朋友,在大马路上碰上了对每个人都是有不同的反应,比如碰上了好基友打招呼都是基情满满,碰上了傻*还有可能会屌他,碰上女神可能会舔一舔等等行为,假如用代码去实现我碰到不同人的反应可能是如下 + +```js +class Myself { + constructor (friendType) { + this.friendType = friendType + } + sayHi () { + if (this.friendType === '基友') { + console.log('你昨天内裤落我家里了') + } else if (this.friendType === '傻*') { + console.log('啥b') + } else if (this.friendType === '女神') { + console.log('周末我能请你吃kfc吗') + } + } +} + +let myself = new Myself('傻*') +myself.sayHi() // 啥b +``` + +能看到sayHi函数显得非常臃肿,如果这时候在加上一个类型的朋友,反应又可能不是一样的,那么还继续会往sayHi函数里添加无尽if-else!这很显然已经违反了开闭原则,我们应该对修改关闭,对拓展开放,下面开始用策略模式重构之前我们在回顾一下策略模式的定义,将算法封装起来,把不变的部分和变化的部分离,这其中我打招呼的方式不会变,会变的是我会遇到不一样的人从而以什么方式打招呼~下面开始代码重构! + +```js +class Myself { + constructor () { + this.strategy = null // 打招呼方式的策略类 + } + sayHi () { + return this.strategy.Hello() + } + setStrategy (strategy) { + this.strategy = strategy + } +} + + +// 定义打招呼的策略类 +class Jiyou { + constructor(name) { + this.name = name + } + Hello () { + console.log(this.name + '你昨天内裤落我家里了') + } +} +class Shabi { + constructor(name) { + this.name = name + } + Hello () { + console.log(this.name + '啥b') + } +} +class Nvshen { + constructor(name) { + this.name = name + } + Hello () { + console.log(this.name + '周末我能请你吃kfc吗') + } +} + +let myself = new Myself() +myself.setStrategy(new Jiyou('基友')) // 设置应用策略类 +myself.sayHi() // 基友你昨天内裤落我家里了 +``` + +能看到各个类之间的职责已经剥离,代码结构已经变得整洁许多,可是这段代码是模仿了传统面向对象语言实现的,为了抽象化才使用了class语法,但其实我们JavaScript可以以更简洁的方式去实现策略模式,上面的Context是Myself,Myself的strategy对象都是由各个策略类创建而来,但是前面一些章节我们有讲到!JavaScript拥有着极其方便的对象创建方式,大可不必这样周转~我们看看用JavaScript如何简化策略模式的重构 + +```js +// 直接使用函数代替class生成的策略类 +var strategy = { + Jiyou: function (name) { + console.log(name + '你昨天内裤落我家里了') + }, + Shabi: function (name) { + console.log(name + '啥b') + }, + Nvshen: function (name) { + console.log(name + '周末我能请你吃kfc吗') + } +} +//myself充当Context +var myself = function (type, name) { + return strategy[type](name) +} +myself('Jiyou', '基友') // 基友你昨天内裤落我家里了 +myself('Nvshen','女神') // 女神周末我能请你吃kfc吗 +``` + +能看到结构又大幅度精简了,因为在JavaScript里函数也属于对象,所以更直接简单方法把strategy定义为函数也能达到策略模式的标准实现 + +## 6.命令模式 + +**核心概念** + +这个模式的核心概念相当简单,用于消除调用者和接收者之间的耦合关系,并且,执行命令过程当中可以进行留痕操作! + +> 还是举个场景例子,我去饭店吃饭,真正给我做菜的人是厨师,可是我总不能直接跑去后厨直接和厨师面对面下单吧,跑去后厨我也不知道哪个人可以给我炒菜QAQ,这个时候服务员出来了,我们可以通过服务员告诉他我想吃什么菜,让他去帮我找厨师给我炒菜,且在下单过程当中,我突然不想吃这个菜了,厨师这会还没开始炒,那么就可以通过服务员进行订单的撤销,这一套流程下来调用者和接收者的解耦工作就是由服务员来完成的,留痕操作则是由订单完成。 + +我们来确定一下一个命令模式的基础功能 + +- 消除调用者和接受者的耦合 +- 命令可以被记录进行回撤 + +下面开始演示命令模式,这里我们用一个动画来进行展示 + +![](https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/01f5305141f0458f8604e4f09e1ac509~tplv-k3u1fbpfcp-zoom-in-crop-mark:4536:0:0:0.awebp) + +直接上代码会更清楚结构 + +```html +
+ +
+

菜单列表

+
    +
  • 西红柿炒鸡蛋
  • +
  • 苦瓜炒鸡蛋
  • +
  • 辣椒炒辣椒
  • +
  • 鸡蛋炒鸡蛋
  • +
+
+ + +
+``` + +```js +// 把这个类抽象成服务员 +class Command { + constructor (commands) { + this.commands = commands // 接收菜单集合 + this.oldCommands = [] // 记录命令用于回撤 + } + execute (type) { + // 调用真实方法并存储id + let id = this.commands[type].execute().id + this.oldCommands.push(id) + } + // 回撤 + undo () { + let id = this.oldCmmands.pop() + console.log('要撤回的订单id是'+id) + } +} +// 菜单集合 +var Menu = { + xhscjd:{ + execute:function () { + console.log('西红柿炒鸡蛋') + let id = 'data-'+Date.now() // 用于标识唯一订单 + return { + id + } + } + } + // ... ... +} + +// 实例化一个服务员帮忙做事 +let waiter = new Command(Menu) +let lis = document.querySelectorAll('.commands>li') +[...lis].map(item => { + item.addEventListener('click', function () { + // 让服务员下单 + waiter.execute(item.getAttribute('cmd')) + }) +}) +``` + +其实你会发现我们好像把简单的事复杂化了,的确如此,完成上述效果并不需要太复杂刻意的去生成一个"服务员"解耦,只需要将分离的命令模块进行直接调用即可,像下面这样 + +```js +[...lis].map(item => { + item.addEventListener('click', function () { + // 直接调用 + const type = item.getAttribute('cmd') + Menu[type].execute() + }) +}) +``` + +**小结** + +命令模式不单单是简单将函数体封装调用,而是通过这种模式给命令去增加撤销操作,像上面demo一样支持撤销订单,也就是说,要基于需求合理使用这个模式,实现这个模式并不困难,如果不清楚一个需求是否需要命令模式,就不要着急实现,只有你真正用到了撤销,恢复等等操作时,这个模式发挥才有意义~~ +