JS进阶9 --JS设计模式
在 JS进阶8 – 函数柯里化I中我们学习了函数柯里化,并对与其相关的经典面试题和实际应用进行了深入剖析。本文将继续跟大家介绍JS中的第9个重点内容 – JS设计模式。废话不多说,一起来看看吧!
JS进阶9 --JS设计模式
这一节咱们来学习JS中的设计模式
传送门:wiki-设计模式
传送门:JavaScript设计模式与开发实践
设计模式指的是:在面向对象软件设计过程中针对特定问题的简洁而优雅的解决方案。通俗一点说,设计模式就是给面向对象软件开发中的一些好的设计取个名字。
目前说到设计模式,一般指的是《设计模式:可复用面向对象软件的基础》一书中提到的23种常见的软件开发设计模式。
JavaScript中不需要生搬硬套这些模式,咱们结合实际前端开发中的具体应用场景,来看看有哪些常用的设计模式。
这一节咱们会学习:
- JS中的常用设计模式
- 设计模式在开发/框架中的应用场景
工厂模式
在JavaScript中,工厂模式的表现形式就是一个直接调用即可返回新对象的函数。
// 定义构造函数并实例化
function Dog(name){
this.name=name
}
const dog = new Dog('柯基')
// 工厂模式
function ToyFactory(name,price){
return {
name,
price
}
}
const toy1 = ToyFactory('布娃娃',10)
const toy2 = ToyFactory('玩具车',15)
应用场景
- Vue2->Vue3:
- 废弃了
new Vue
,改成了工厂函数createApp
-传送门 - 任何全局改变 Vue 行为的 API(vue2) 现在都会移动到应用实例上(vue3)
- 就不会出现,Vue2中多个Vue实例共享,相同的全局设置,可以实现隔离。
- 废弃了
<!DOCTYPE html>
<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. 基于不同基地址创建多个 请求对象
const request1 = axios.create({
baseURL: "基地址1"
})
const request2 = axios.create({
baseURL: "基地址2"
})
const request3 = axios.create({
baseURL: "基地址3"
})
// 2. 通过对应的请求对象,调用接口即可
request1({
url: '基地址1的接口'
})
request2({
url: '基地址2的接口'
})
request3({
url: '基地址3的接口'
})
总结
- 工厂模式:JS中的表现形式,返回新对象的函数(方法)
function sayHi(){} // 函数
const obj ={
name:'jack',
sayHello(){} // 方法
}
- 日常开发中,有2个很经典的场景
vue3
中创建实例的api改为createApp
,vue2
中是new Vue
。- Vue3中,没有影响所有Vue实例的api了,全都变成了影响某个app对象的api,比如
Vue.component-->app.component
。
- Vue3中,没有影响所有Vue实例的api了,全都变成了影响某个app对象的api,比如
axios.create
基于传入的配置,创建一个新的请求对象,可以用来设置多个基地址。
单例模式
单例模式指的是,在使用这个模式时,单例对象整个系统需要保证只有一个存在。
需求
- 通过静态方法
getInstance
获取唯一实例
const s1 = SingleTon.getInstance()
const s2 = SingleTon.getInstance()
console.log(s1===s2)//true
核心步骤
- 定义类
- 私有静态属性:
#instance
- 提供静态方法
getInstance
:- 调用时判断
#instance
是否存在: - 存在:直接返回
- 不存在:实例化,保存,并返回
- 调用时判断
class SingleTon {
constructor() { }
// 私有属性,保存唯一实例
static #instance
// 获取单例的方法
static getInstance() {
if (SingleTon.#instance === undefined) {
// 内部可以调用构造函数
SingleTon.#instance = new SingleTon()
}
return SingleTon.#instance
}
}
实际应用
- vant组件库中的弹框组件,保证弹框是单例
- vue中注册插件,用到了单例的思想(只能注册一次)
总结
- 单例模式:
- 保证,应用程序中,某个对象,只能有一个
- 自己实现核心为一个返回唯一实例的方法,比如
getInstance
- 实例存在
->
返回 - 实力不存在
->
创建,保存->
返回
- 实例存在
- 应用场景:
vant
的toast
和notify
组件都用到了单例:多次弹框,不会创建多个弹框,复用唯一的弹框对象vue
中注册插件,vue2
和vue3
都会判断插件是否已经注册,已注册,直接提示用户
观察者模式
在对象之间定义一个一对多的依赖,当一个对象状态改变的时候,所有依赖的对象都会自动收到通知。
应用场景
1. dom
事件绑定,比如
window.addEventListener('load', () => {
console.log('load触发1')
})
window.addEventListener('load', () => {
console.log('load触发2')
})
window.addEventListener('load', () => {
console.log('load触发3')
})
2. Vue中的watch:
总结
- 观察者模式重点说清楚2点即可:
- 在对象之间定义一个一对多的依赖,当一个对象状态改变的时候,所有依赖的对象都会自动收到通知。
- 常见场景:vue中的watch,dom事件绑定
观察者模式和发布订阅模式的区别也是常见考点,回答方式见下一节
发布订阅模式
发布订阅模式可以实现的效果类似观察者模式,但是两者略有差异,一句话描述:一个有中间商(发布订阅模式)一个没中间商(观察者模式)
应用场景
手写发布订阅模式
需求
const bus = new MyEmmiter()
// 注册事件
bus.$on('事件名1',回调函数)
bus.$on('事件名1',回调函数)
// 触发事件
bus.$emit('事件名',参数1,...,参数n)
// 移除事件
bus.$off('事件名')
// 一次性事件
bus.$once('事件名',回调函数)
核心步骤
- 定义类
- 私有属性:
#handlers={事件1:[f1,f2],事件2:[f3,f4]}
- 实例方法:
- $on(事件名,回调函数):注册事件
- $emit(事件名,参数列表):触发事件
- $off(事件名):移除事件
- $once(事件名,回调函数):注册一次性事件
基础模板:
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<h2>自己实现事件总线</h2>
<button class="on">注册事件</button>
<button class="emit">触发事件</button>
<button class="off">移除事件</button>
<button class="once-on">一次性事件注册</button>
<button class="once-emit">一次性事件触发</button>
<script>
class MyEmmiter {
// 逻辑略
}
// 简化 querySelector调用
function qs(selector) {
return document.querySelector(selector)
}
// 注册事件
qs('.on').addEventListener('click', () => {
})
// 触发事件
qs('.emit').addEventListener('click', () => {
})
// 移除事件
qs('.off').addEventListener('click', () => {
})
// 一次性事件注册
qs('.once-on').addEventListener('click', () => {
})
// 一次性事件触发
qs('.once-emit').addEventListener('click', () => {
})
</script>
</body>
</html>
class MyEmmiter {
#handlers = {}
// 注册事件
$on(event, callback) {
if (!this.#handlers[event]) {
this.#handlers[event] = []
}
this.#handlers[event].push(callback)
}
// 触发事件
$emit(event, ...args) {
const funcs = this.#handlers[event] || []
funcs.forEach(func => {
func(...args)
})
}
// 移除事件
$off(event) {
this.#handlers[event] = undefined
}
// 一次性事件
$once(event, callback) {
this.$on(event, (...args) => {
callback(...args)
this.$off(event)
})
}
}
总结
- 发布订阅模式:可以实现的效果类似观察者模式,但是两者略有差异,一句话描述:一个有中间商(发布订阅模式)一个没中间商(观察者模式)
- 经典的场景是
vue2
中的EventBus
,vue3
移除了实例的$on
,$off
,$emit
方法,如果还需要使用:- 使用第三方插件
- 自己实现事件总线:
- 自己实现事件总线的核心逻辑:
- 添加类,内部定义私有属性
#handlers={}
,以对象的形式来保存回调函数 - 添加实例方法:
$on
:- 接收事件名和回调函数
- 内部判断并将回调函数保存到
#handlers
中,以{事件名:[回调函数1,回调函数2]}
格式保存
$emit
- 接收事件名和回调函数参数
- 内部通过
#handlers
获取保存的回调函数,如果获取不到设置为空数组[]
- 然后挨个调用回调函数即可
$off
- 接收事件名
- 将
#handlers
中事件名对应的值设置为undefined
即可
$once
- 接收事件名和回调函数
- 内部通过
$on
注册回调函数, - 内部调用
callback
并通过$off
移除注册的事件
- 添加类,内部定义私有属性
原型模式
在原型模式下,当我们想要创建一个对象时,会先找到一个对象作为原型,然后通过克隆原型的方式来创建出一个与原型一样(共享一套数据/方法)的对象。在JavaScript
中,Object.create
就是实现原型模式的内置api
。
应用场景
vue2
中重写数组方法:
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<h2>原型模式</h2>
<div id="app"></div>
<script src="https://cdn.bootcdn.net/ajax/libs/vue/2.7.9/vue.js"></script>
<script>
const app = new Vue({
el: "#app", data: {
foods: ['西瓜', '西葫芦', '西红柿']
}
})
console.log(app.foods.push === Array.prototype.push)
</script>
</body>
</html>
总结
- 原型模式:
- 基于某个对象,创建一个新的对象
- JS中,
Object.create
就是实现了这个模式的内置api
- 比如
vue2
中重写数组方法就是这么做的
- vue2中数组重写了7个方法,内部基于数组的原型
Array.prototype
创建了一个新对象 - 创建的方式是通过
Object.create
进行浅拷贝 - 重写的时候:
- 调用数组的原方法,获取结果并返回—方法的功能和之前一致
- 通知了所有的观察者去更新视图
const app = new Vue({
el:"#app",
data:{
arr:[1,2,3]
}
})
app.arr.push === Array.prototype.push //false
代理模式
代理模式指的是拦截和控制与目标对象的交互。
这里我们来看一个非常经典的代理模式的应用: 缓存代理
核心语法
- 创建对象缓存数据
- 获取数据时,先通过缓存判断:
- 有直接获取
- 没有:调用接口
// 1. 创建对象缓存数据
const cache = {}
async function searchCity(pname) {
// 2. 判断是否缓存数据
if (!cache[pname]) {
// 2.1 没有:查询,缓存,并返回
const res = await axios({
url: 'http://hmajax.itheima.net/api/city',
params: {
pname
}
})
cache[pname] = res.data.list
}
// 2.2 有:直接返回
return cache[pname]
}
document.querySelector('.query').addEventListener('keyup', async function (e) {
if (e.keyCode === 13) { // 回车键
const city = await searchCity(this.value)
console.log(city)
}
})
总结
- 代理模式的核心是,通过一个代理对象拦截对原对象的直接操纵。
- 比如可以通过缓存代理:
- 缓存获取到的数据
- 拦截获取数据的请求:
- 已有缓存:直接返回缓存数据
- 没有缓存:去服务器获取数据并缓存
- 提升数据获取效率,降低服务器性能消耗。
迭代器模式
迭代器模式提供一种方法顺序访问一个聚合对象中的各个元素,而又不暴露该对象的内部表示.简而言之就是:遍历。
遍历作为日常开发中的高频操作,JavaScript中有大量的默认实现:比如
Array.prototype.forEach
:遍历数组NodeList.prototype.forEach
:遍历dom
,document.querySelectorAll
for in
for of
面试题
1. for in
和for of
的区别?
for...in
语句以任意顺序迭代一个对象的除Symbol以外的可枚举属性,包括继承的可枚举属性。- 对象默认的属性以及动态增加的属性都是可枚举属性
- 遍历出来的是属性名
- 继承而来的属性也会遍历
for...of
语句在可迭代对象(包括Array
,Map
,Set
,String
,TypedArray
,arguments 对象等等)上创建一个迭代循环。- for of不会遍历继承而来的属性
- 遍历出来的是属性值
Object.prototype.objFunc = function () { }
Array.prototype.arrFunc = 'arrFunc'
const foods = ['西瓜', '西葫芦', '西兰花']
for (const key in foods) {
console.log('for-in:key', key)
}
for (const iterator of foods) {
console.log('for-of:iterator', iterator)
}
2. 可迭代协议和迭代器协议
- 可迭代协议:传送门
- 给对象增加方法
[Symbol.iterator](){}
- 返回一个符合迭代器协议的对象
- 给对象增加方法
- 迭代器协议:传送门
- next方法,返回对象:
{done:true}
,迭代结束{done:false,value:'xx'}
,获取解析并接续迭代- 实现方式:
- 手写
Generator
- next方法,返回对象:
// ------------- 迭代协议 -------------
/**
* 迭代协议可以定制对象的迭代行为 分为2个协议:
* 1. 可迭代协议: 增加方法[Symbol.iterator](){} 返回符合 迭代器协议 的对象
* 2. 迭代器协议:
* 有next方法的对象,next方法返回:
* 已结束: {done:true}
* 继续迭代: {done:false,value:'x'}
* 使用Generator
* 自己实现 对象,next
* */
const obj = {
// Symbol.iterator 内置的常量
// [属性名表达式]
[Symbol.iterator]() {
// ------------- 自己实现 -------------
const arr = ['北京', '上海', '广州', '深圳']
let index = 0
return {
next() {
if (index < arr.length) {
// 可以继续迭代
return { done: false, value: arr[index++] }
}
// 迭代完毕
return { done: true }
}
}
// ------------- 使用Generator -------------
// function* foodGenerator() {
// yield '西兰花'
// yield '花菜'
// yield '西兰花炒蛋'
// }
// const food = foodGenerator()
// return food
}
}
for (const iterator of obj) {
console.log('iterator:', iterator)
}
总结
- 迭代器模式在js中有大量的默认实现,因为遍历或者说迭代是日常开发中的高频操作,比如
forEach
,for in
,for of
等。 for in
和for of
的区别:- 如何自定义可迭代对象?