行业知识
【JavaScript】设计模式
JS设计模式
��送门:wiki-设计模式
传送门:JavaScript设计模式与开发实践
设计模式的指的是:在面向对象软件设计过程中针对特定问题的简洁而优雅的解决方案。通俗一点说,设计模式就是给面向对象软件开发中的一些好的设计取个名字。
目前说到设计模式,一般指的是《设计模式:可复用面向对象软件的基础》一书中提到的23种常见的软件开发设计模式。
工厂模式
在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实例共享,相同的全局设置,可以实现隔离
Document #app1, #app2 { border: 1px solid #000; } vue2-全局注册组件
实例1 实例2 Vue.component('my-title', { template: '标题组件
' }) const app1 = new Vue({ el: "#app1" }) const app2 = new Vue({ el: "#app2" }) -
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
-
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组件库中的弹框组件,保证弹框是单例
-
toast组件:传送门
-
notify组件:传送门
-
如果弹框对象
- 不存在,–>创建一个新的
- 存在,直接用
-
-
vue中注册插件,用到了单例的思想(只能注册一次)
- vue2:传送门
- vue3:传送门
面试回答:
-
单例模式:
- 保证,应用程序中,某个对象,只能有一个
-
自己实现核心为一个返回唯一实例的方法,比如getInstance
- 实例存在->返回
- 实力不存在->创建,保存->返回
-
应用场景:
- vant的toast和notify组件都用到了单例:多次弹框,不会创建多个弹框,复用唯一的弹框对象
- vue中注册插件,vue2和vue3都会判断插件是否已经注册,已注册,直接提示用户
观察者模式
在对象之间定义一个一对多的依赖,当一个对象状态改变的时候,所有依赖的对象都会自动收到通知。
举个例子:
- dom事件绑定,比如
window.addEventListener('load', () => { console.log('load触发1')})window.addEventListener('load', () => { console.log('load触发2')})window.addEventListener('load', () => { console.log('load触发3')}) - Vue中的watch:
面试回答:
-
观察者模式重点说清楚2点即可:
- 在对象之间定义一个一对多的依赖,当一个对象状态改变的时候,所有依赖的对象都会自动收到通知。
- 常见场景:vue中的watch,dom事件绑定
观察者模式和发布订阅模式的区别也是常见考点,回答方式见下一节
发布订阅模式01-应用场景
发布订阅模式可以实现的效果类似观察者模式,但是两者略有差异,一句话描述:一个有中间商(发布订阅模式)一个没中间商(观察者模式)
应用场景:
-
vue2中的EventBus:传送门
-
vue3中因为移除了实例上对应方法,可以使用替代方案:传送门
- 官方推荐,用插件
- 我们自己写
发布订阅模式02-自己写一个事件总线
需求:
const bus = new HMEmitter()// 注册事件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(事件名,回调函数):注册一次性事件
基础模板:
Document自己实现事件总线
注册事件 触发事件 移除事件 一次性事件注册 一次性事件触发 class HMEmmiter { // 逻辑略 } // 简化 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', () => { })
class HMEmmiter { #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中重写数组方法:
- 调用方法时(push,pop,shift,unshift,splice,sort,reverse)可以触发视图更新:传送门
- 源代码:传送门
- 测试一下:
Document原型模式
const app = new Vue({ el: "#app", data: { foods: ['西瓜', '西葫芦', '西红柿'] } }) console.log(app.foods.push === Array.prototype.push)
面试回答:
-
原型模式:
- 基于某个对象,创建一个新的对象
- 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
面试题:
-
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)} 可迭代协议和迭代器协议:
-
可迭代协议:传送门
- 给对象增加属方法[Symbol.iterator](){}
- 返回一个符合迭代器协议的对象
-
迭代器协议:传送门
-
next方法,返回对象:
-
{done:true},迭代结束
-
{done:false,value:'xx'},获取解析并接续迭代
-
实现方式:
- 手写
- Generator
-
-
// ------------- 迭代协议 -------------/** * 迭代协议可以定制对象的迭代行为 分为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 面试回答:
-
迭代器模式在js中有大量的默认实现,因为遍历或者说迭代时日常开发中的高频操作,比如forEach,for in,for of等
-
for in 和for of的区别:
-
for...in 语句以任意顺序迭代一个对象的除Symbol以外的可枚举属性,包括继承的可枚举属性。
- 对象默认的属性以及动态增加的属性都是可枚举属性
- 遍历出来的是属性名
- 继承而来的属性也会遍历
-
for...of语句在可迭代对象(包括 Array,Map,Set,String,TypedArray,arguments 对象等等)上创建一个迭代循环
- for of不会遍历继承而来的属性
- 遍历出来的是属性值
-
如何自定义可迭代对象?
-
需要符合2个协议:可迭代协议和迭代器协议,其实就是按照语法要求实现功能而已
-
可迭代协议:传送门
- 给对象增加属方法[Symbol.iterator](){}
- 返回一个符合迭代器协议的对象
-
迭代器协议:传送门
-
有next方法的一个对象,内部根据不同情况返回对应结果:
- {done:true},迭代结束
- {done:false,value:'xx'},获取解析并接续迭代
-
实现方式:
- 自己手写实现逻辑
- 直接返回一个Generator
参考资料
- 阮一峰-《ECMAScript 6 教程》
- 图灵社区-JavaScript高级程序设计