设计模式
设计模式
kilitoJS设计模式
设计模式的指的是:在面向对象软件设计过程中针对特定问题的简洁而优雅的解决方案。通俗一点说,设计模式就是给面向对象软件开发中的一些好的设计取个名字。
目前说到设计模式,一般指的是《设计模式:可复用面向对象软件的基础》一书中提到的23种常见的软件开发设计模式。
JavaScript中不需要生搬硬套这些模式,咱们结合实际前端开发中的具体应用场景,来看看有哪些常用的设计模式
这一节咱们会学习:
- JS中的常用设计模式
- 设计模式在开发/框架中的应用场景
工厂模式
在JavaScript中,工厂模式的表现形式就是一个直接调用即可返回新对象的函数
1 | // 定义构造函数并实例化 |
应用场景
Vue2->Vue3:
- 启用了
new Vue
,改成了工厂函数createApp
-传送门 - 任何全局改变 Vue 行为的 API(vue2) 现在都会移动到应用实例上(vue3)
- 就不会出现,Vue2中多个Vue实例共享,相同的全局设置,可以实现隔离
- 避免vue2中全局设置的东西,比如组件,影响后续实例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<style>
#app1,
#app2 {
border: 1px solid #000;
}
</style>
</head>
<body>
<h2>vue2-全局注册组件</h2>
<div id="app1">
实例1 组件
<my-title></my-title>
</div>
<div id="app2">
实例2
<my-title></my-title>
</div>
<script src="https://cdn.bootcdn.net/ajax/libs/vue/2.7.9/vue.js"></script>
<script>
// 全局注册组件
Vue.component('my-title', {
// 组件的结构
template: '<h2 style="color:orange">标题组件</h2>'
})
const app1 = new Vue({
el: "#app1"
})
const app2 = new Vue({
el: "#app2"
})
</script>
</body>
</html>- 启用了
axios.create:
- 基于传入的配置创建一个新的
axios
实例,传送门 - 项目中有2个请求基地址如何设置?
- 创建出多个请求不同的对象,比如设置多个基地址
- 基于传入的配置创建一个新的
1 | // 1. 基于不同基地址创建多个 请求对象 |
小结:
- 工厂模式:JS中的表现形式,返回新对象的函数(方法)
1 | function sayHi(){} // 函数 |
日常开发中,有2个很经典的场景
vue3
中创建实例的api改为createApp
,vue2
中是new Vue
- Vue3中,没有影响所有Vue实例的api了,全都变成了影响某个app对象的api,比如
Vue.component-->app.component
- Vue3中,没有影响所有Vue实例的api了,全都变成了影响某个app对象的api,比如
axios.create
基于传入的配置,创建一个新的请求对象,可以用来设置多个基地址
单例模式
单例模式指的是,在使用这个模式时,单例对象整个系统需要保证只有一个存在。
需求:
- 通过静态方法
getInstance
获取唯一实例
1 | const s1 = SingleTon.getInstance() |
核心步骤:
- 定义类
- 私有静态属性:
#instance
- 提供静态方法
getInstance
:- 调用时判断
#instance
是否存在: - 存在:直接返回
- 不存在:实例化,保存,并返回
- 调用时判断
1 | class SingleTon { |
实际应用:
- vant组件库中的弹框组件,保证弹框是单例
- vue中注册插件,用到了单例的思想(只能注册一次)
小结:
单例模式:
- 保证,应用程序中,某个对象,只能有一个
自己实现:
- getInstance方法,
- 实例存在->返回
- 实例不存在->创建,保存->返回
- getInstance方法,
应用场景:
- 我在看源码的时候,发现,vant的toast和notify组件都用到了单例
- 多次弹框,不会创建多个弹框,复用唯一的弹框对象
- vue中注册插件,vue3和vue2都会判断插件是否已经注册,已注册,直接提示用户
- 我在看源码的时候,发现,vant的toast和notify组件都用到了单例
观察者模式
在对象之间定义一个一对多的依赖,当一个对象状态改变的时候,所有依赖的对象都会自动收到通知。
举个例子:
dom
事件绑定,比如
1 | window.addEventListener('load', () => { |
Vue的生命周期钩子:
- vue框架,提供给开发者,在Vue实例特定时期,添加自定义逻辑的,一种机制.
- 一共有:
- beforeCreated
- created
- beforeMount
- Mounted
- beforeUpdate
- Updated
- beforeDestory
- destoryed
- 缓存组件(keep-alive):activated deactivated
Vue的响应式原理:
自己描述:响应式原理
创建Vue实例时,会通过Object.definedProperty将data中的数据的每个属性都转为get和set
就可以监测到对数据的,取值get和赋值set
只要数据发生了变更,就会去通知所有使用数据为止,更新
- 页面
- 侦听器
- …
自己描述2-涉及到虚拟dom:
- 可能被追问:
- 为什么需要使用虚拟dom? 虚拟dom内存中,速度快
- 新旧dom比较,如何比较的?
- diff算法 找不同
- vue中同级比较
- 有id,id不同,直接不同
- 没有id,比元素,在比属性
- vue的diff算法和react的diff算法有什么区别?
- 可能被追问:
自己回答:
- vue2中使用的是 Object.definedProperty,动态新增的属性,没有响应式,this.$set
- vue3中是Proxy
- 没有这个问题,Proxy可以检测到所有属性的改变
- vue3中只用了 Proxy 吗?不是,引用了
Object.definedProperty
发布订阅模式01-应用场景
发布订阅模式可以实现的效果类似观察者模式,但是两者略有差异,一句话描述:一个有中间商(发布订阅模式)一个没中间商(观察者模式)
应用场景:
发布订阅模式02-自己写一个事件总线
需求:
1 | const bus = new HMEmitter() |
核心步骤:
- 定义类
- 私有属性:
#handlers={事件1:[f1,f2],事件2:[f3,f4]}
- 实例方法:
- $on(事件名,回调函数):注册事件
- $emit(事件名,参数列表):触发事件
- $off(事件名):移除事件
- $once(事件名,回调函数):注册一次性事件
基础模板:
1 |
|
1 | class HMEmmiter { |
原型模式
在原型模式下,当我们想要创建一个对象时,会先找到一个对象作为原型,然后通过克隆原型的方式来创建出一个与原型一样(共享一套数据/方法)的对象。在JavaScript
中,Object.create
就是实现原型模式的内置api
应用场景:
vue2
中重写数组方法:
1 |
|
自己描述:
- vue2中数组重写了7个方法,内部基于数组的原型
Array.prototype
创建了一个新对象 Object.create
浅拷贝- 内部
- 调用数组的原方法,获取结果并返回—方法的功能和之前一致
- 通知了所有的观察者去更新视图
1 | const app = new Vue({ |
- 原型模式,基于某个对象,创建一个新的对象,JS中,通过Object.create即可实现,Vue中重写数组方法就是这么做的 ↑
代理模式
代理模式指的是拦截和控制与目标对象的交互,在JavaScript
中通过Proxy
,即可实现对象的代理,传送门
核心语法:
- 初始对象可以直接修改任意属性
- 通过
Proxy
生成代理对象,限制访问
1 | // 目前obj对象的name和age属性可以被随意修改 |
需求:
基于上一份代码实现:
- 属性取值和赋值时,如果属性不存在,报错
- 修改name时,只能设置字符串,否则报错
关键步骤:
- 在
get
中添加取值判断逻辑 - 在
set
中添加赋值判断逻辑
1 | // 目前obj对象的name和age属性可以被随意修改 |
实际应用:
Vue3
的响应式原理-传送门
通过
Proxy
创建响应式对象getter/setter
用于ref
Vue2考虑兼容,用的是兼容性好的
Object.defineProperty
,但是无法跟踪动态增加的属性Vue3
中用了Proxy
,他对于动态增加的属性,也可以检测到,但是Vue3中也用了Object.defineProperty
reactive
用的是Proxy
- 注意点:解构之后会丢失响应性,需要用
toRefs
- 注意点:解构之后会丢失响应性,需要用
ref
用的是Object.defineProperty
观察者模式–>虚拟dom->diff算法
迭代器模式
迭代器模式提供一种方法顺序访问一个聚合对象中的各个元素,而又不暴露该对象的内部表示.简而言之就是:遍历
遍历作为日常开发中的高频操作,JavaScript中有大量的默认实现:比如
Array.prototype.forEach
:遍历数组NodeList.prototype.forEach
:遍历dom
,document.querySelectorAll
for in
for of
面试题:
for in
和for of
的区别?for...in
语句以任意顺序迭代一个对象的除Symbol以外的可枚举属性,包括继承的可枚举属性。- 对象默认的属性以及动态增加的属性都是可枚举属性
- 遍历出来的是属性名
- 继承而来的属性也会遍历
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20<script>
// 原型上默认的属性和方法,都是不可枚举(for in不出来)
// 动态添加的,默认是可枚举(可以for in出来)
Object.prototype.run = function() {
console.log('奔跑')
}
Array.prototype.swim = '游泳'
const arr = ['小鸡', '小鸭', '小鱼']
// 遍历的是key,继承而来的属性也可以遍历出来
for (const key in arr) {
console.log('key:', key) // key: 0 key: 1 key: 2 key: swim key: run
}
// 遍历的值,继承而来的遍历不出来
for (const iterator of arr) {
console.log('iterator:', iterator) // key: 小鸡 key: 小鸭 key: 小鱼
}
</script>for...of
语句在可迭代对象(包括Array
,Map
,Set
,String
,TypedArray
,arguments 对象等等)上创建一个迭代循环- for of不会遍历继承而来的属性
- 遍历出来的是属性值
1 | const arr = ['小鸡', '小鸭', '小鱼'] |
可迭代协议和迭代器协议:
可迭代协议:传送门
- 给对象增加属方法
[Symbol.iterator](){}
- 返回一个符合迭代器协议的对象
- 给对象增加属方法
迭代器协议:传送门
- next方法,返回对象:
{done:true}
,迭代结束{done:false,value:'xx'}
,获取解析并接续迭代
- next方法,返回对象:
面试问及:
- for of可以遍历一部分的类型,比如数组,map
- 对象无法遍历,因为对象没有实现 可迭代协议,迭代器协议
- 可迭代协议,迭代器协议,约定了:
- 可迭代协议:对象上要有一个指定属性的函数,返回 满足迭代器要求的对象
- 迭代器协议:
next
方法,返回{done:true},{done:false,value:'x'}
- 我自己尝试写过一下,但是仅针对语法
- 可以和面试官讨论一下,可以用在哪?
直接打印对象,看到Symbol(Symbol.iterator),说明可以使用
for of
JS调用栈
- 执行上下文和调用栈
- 栈溢出
执行上下文和调用栈
执行上下文:是指在代码执行时,JavaScript引擎创建的一种数据结构,它包含了函数执行时的状态信息,例如变量、函数参数、函数返回值等。
在以下三种情况下会创建执行上下文
JavaScript执行全局代码时,创建全局执行上下文
调用函数时,创建函数执行上下文
使用 eval 函数时,创建执行上下文
- 给他一个字符串,解析为js并执行
我们通过调试工具确认一下
1 | const num = 0 |
调用栈:
- 执行上下文会存在JS调用栈中,栈的结构特点是:先进后出
栈溢出
栈的容量是有限的,如果内部的内容一直得不到释放,就会出现栈溢出,比如
1 | // 栈溢出,JS调用栈有容量大小,太大了,会溢出 |