> 文章列表 > 设计模式和垃圾回收

设计模式和垃圾回收

设计模式和垃圾回收

系列文章目录

前端系列文章——传送门
JavaScript系列文章——传送门


文章目录

  • 系列文章目录
  • 设计模式和垃圾回收
    • 一、设计模式
      • 1、设计模式介绍
      • 2、单例模式
      • 3、组合模式
      • 4.发布订阅模式
      • 5、观察者模式
    • 二、垃圾回收
      • 1、垃圾回收介绍
      • 2、标记清除法
      • 3、标记整理法
      • 4、引用计数清除法
      • 5、性能优化

设计模式和垃圾回收

一、设计模式

1、设计模式介绍

设计模式是面向对象中反复多次出现的问题,而总结出来的最优解决方案。

例如:我们使用字面量方式创建对象,容易造成重复,这个在应用过程中是不可避免的,为了解决这个问题,我们通过一个函数来解决重复,也就是工厂函数。此时就可以把这套方案叫做工厂模式。

不同的问题,总结出的方案也是不同的。在当前的实际应用过程中,人们总结了23种设计模式。常见的设计模式有:

  • 单例模式
  • 组合模式
  • 观察者模式
  • 发布订阅模式
  • 命令模式
  • 代理模式
  • 工厂模式
  • 策略模式
  • 适配器模式

2、单例模式

单例模式是指,通过一个类衍生出的对象,只能有一个,不会有第二个,就算有第二个,跟第一个是共享同一个堆地址的。

单例模式解决的问题是,当我们多次调用一个类中方法的时候,通常需要实例化多次,然后调用方法。但其实每次使用的方法有一个类就已经能用了,实例化多次,得到的多个实例化对象,造成了内存浪费,所以需要单例模式:

class Single{}

将一个类放在全局,不可避免的,就会被实例化多次,所以,放在全局不能实现单例模式。

function fn() {class Single{}var s = new Single()return s
}

放在局部,每次调用函数,会重新实例化类得到新的s变量,跟放在全局的效果是一样的。如何确保多次调用函数,在使用同一个s变量 - 闭包:

function fn() {class Single{}var sreturn function() {if(!s) {s = new Single  }return s}
}
var fun = fn()
var s1 = fun()
var s2 = fun()
console.log(s1 === s2) // true

此时创建对象只能通过调用fn中的返回函数,且多次调用使用的是同一个数据s。

但fn在全局是可以被调用多次的,每次调用都会创建一个独立的不会销毁的执行空间,也就是会得到多个不同的小函数,调用不同的小函数得到的对象也是独属于不销毁的执行空间的,还是会有不同:

function fn() {class Single{}var sreturn function() {if(!s) {s = new Single  }return s}
}
var fun = fn()
var s1 = fun()
var ff = fn()
var s2 = ff()
console.log(s1 === s2) // false

为了保证fn只能被调用一次,将fn作为自调用函数:

var fun = (function() {class Single{}var sreturn function() {if(!s) {s = new Single  }return s}
})()
var s1 = fun()
var s2 = fun()
console.log(s1 === s2) // true

此时创建对象只能调用fun,且每次调用共用同一个s变量,这样就形成了最终的单例模式。

3、组合模式

组合模式指的是,每个面向对象效果,在开启的时候,都需要先实例化对象,然后调用实例化对象初始化方法开启。有多个实例对象,我们就需要实例化多次并调用多次初始化方法。

如果将初始化方法比作是一个开灯的开关,我们就可以将多个开关组成一个组合开关,只要开启组合开关就让所有开关都开启。组合模式就是要制作这样一个组合开关。

class Carousel{init() {console.log('这是轮播图的初始化方法')}
}class Tab{init() {console.log('这是tab切换的初始化方法')}
}class Enlarge{init() {console.log('这是放大镜的初始化方法')}
}

如何将这3个效果的初始化方法调用作为一个组合开关?

思路:既然要组合,就需要将这3个单独的开关组合在一起。将这个3个类的实例化对象,都放在同一个容器中,遍历容器中的每个效果对象,调用初始化方法:

class Combination{// 定义属性container作为效果对象存放的容器container = []// 将效果对象放在容器中的方法add(...arr) {if(!arr.length) return this.container = this.container.concat(arr)}// 组合开关init() {// 遍历容器this.container.forEach(item => {// 批量调用item.init()})}
}var c = new Combination()
c.add(new Tab, new Carousel, new Enlarge)
c.init()

如果效果对象的初始化不叫init,如何更加灵活的做组合开关呢?

class Carousel{init() {console.log('这是轮播图的初始化方法')}
}class Tab{start() {console.log('这是tab切换的初始化方法')}
}class Enlarge{go() {console.log('这是放大镜的初始化方法')}
}
class Combination{// 定义属性container作为效果对象存放的容器container = []// 将效果对象放在容器中的方法add(...arr) {if(!arr.length) return this.container = this.container.concat(arr)}// 组合开关init() {// 遍历容器this.container.forEach(item => {// 批量调用for(var key in item) {item[key][key]()}})}
}var c = new Combination()
c.add({init: new Tab}, {start: new Carousel}, {go: new Enlarge})
c.init()

4.发布订阅模式

发布订阅模式分为两个概念,一个是发布者,一个订阅者。当发布者发布消息后,订阅过这个消息的订阅者能接收到这个消息并执行一定的操作。

例如:小明到商店买煊赫门香烟,但是煊赫门卖完了,小明就在商店进行登记,当商店有货的时候,通知小明,小明就可以去买了。

发布者:

class Publisher{// 要收集订阅者登记消息,所以需要容器订阅者,订阅者可能不止一个,所以需要定义一个数组subscribers = []// 需要一个方法将订阅者放在容器中addSub(...arr) {this.subscribers.push(...arr)}// 需要一个方法通知订阅者到货了notify(message) { // message - 发布的的消息// 订阅者们就可以做自己的事情了this.subscribers.forEach(item => {// 订阅者接收到消息后执行自己的方法if(item.message === message) { // 发布的消息和订阅的消息相同item.execute() // 执行发布者的动作}})}
}

订阅者:

class Subscriber{// message - 订阅的消息// cb - 要执行的函数constructor(message, cb) {this.message = messagethis.execute = cb        }
}

使用示例:

// 创建发布者
var pub = new Publisher()
// 创建订阅者:订阅消息(购买煊赫门) 执行的函数
var sub1 = new Subscriber('购买煊赫门', function() {console.log('到货了可以购买了');
})
// 发布者等级订阅者
pub.addSub(sub)
// 发布者发布消息(要跟订阅的是同一个消息)
pub.notify('购买煊赫门')var sub2 = new Subscriber('数据获取', function() {console.log('渲染页面');
})
pub.addSub(sub2)
pub.notify('数据获取')

订阅者要执行的代码,什么时候发布者发布了消息才会执行。

5、观察者模式

观察者模式的含义,是一个观察者在观察某件事件的进展,当进行到一定程度的时候就会执行某个操作。

例如:我要做核酸,但是做核酸的人比较多,我就不停的观察,当排队的人少的时候,我就去做。

class Watcher{// 定义存储观察的事情和函数的容器container = {}// 需要有添加观察事情的方法bind(type, handler) { // type为观察的事情;handler为要执行的函数// 需要将观察的事情和要执行的函数存下来,以便后面拿出来执行if(!this.container[type]) { // 判断是否观察过this.container[type] = [] // 没有观察过,就给对应的值赋值为空数组}// 给数组添加要执行的函数this.container[type].push(handler)}   // 触发事情的进展touch(type) { // type为事情的类型if(!this.container[type]) { // 判断是否观察过return}// 遍历观察的事情对应的所有函数并调用this.container[type].forEach(item => {item()})}// 这件事情结束后要取消观察的事情对应的某个函数unbind(type, handler) {if(!this.container[type]) { // 判断是否观察过return}// 找到这个函数在数组中的下标var index = this.container[type].findIndex(item => item === handler)// 从数组中删除这个函数index >= 0 && this.container[type].splice(index, 1)// 如果删除后数组为空,就将这个观察的事情取消if(!this.container[type].length) {delete this.container[type]}}// 取消观察clear(type) {delete this.container[type] // 删除键值对}
}

使用示例:

// 创建观察者
var w = new Wathcer()
// 观察事件a1, 执行fa1函数
w.bind('a1', fa1)
function fa1() {consolel.log('时机到了,该执行了1')
}
// 观察事件a1添加要执行fa2函数
w.bind('a1', fa2)
function fa2() {consolel.log('时机到了,该执行了2')
}
// 触发a1事件
w.touch('a1')
// 取消执行函数fa1
w.unbind('a1', fa1)
// 触发a1事件
w.touch('a1')
// 取消观察a1事件
w.clear()
// 触发a1事件
w.touch('a1')

二、垃圾回收

1、垃圾回收介绍

任何代码的执行,都需要在内存中进行,代码运行结束后,就需要释放内存,否则随着代码运行的数量增多,有限的内存会承受不住太大的负担而崩溃。

浏览器为了释放js中没有用的内存空间,设计了专业的垃圾回收机制。

垃圾回收机制,不是即时处理垃圾内存的,而是有周期性的,隔一段时间,处理一次。

被内存空间中,页面在打开状态时,全局变量的内存是不会被释放的,因为在页面正在运行时,全局变量随时都可能会使用。全局变量的内存会在页面关闭后被回收。

所以这里要介绍的垃圾回收机制,主要是指局部变量和执行空间。

js的垃圾回收机制,全称(Garbage Collecation),简称GC

2、标记清除法

现如今大部分浏览器所使用的垃圾回收机制。

标记清除回收机制的原理:

  1. 对所有数据的内存空间遍历,对所有能访问到的数据的内存空间做标记
  2. 遍历所有数据的内存空间,将没有做标记的数据空间释放,并还原第1步操作

例:

var a = 1
var b = a
a = nullvar c = new Object()
var d = c
c = null 

将a赋值为null,数字1在内存中占用的空间会在下次回收垃圾的时候回收掉。c赋值为null,原本new出来的实例对象所在内存空间还在被变量d使用,所有不会被回收。

这种回收机制的缺点,就是在清除无用的空间后,有数据的空间是不连续的,造成某些内存空间无法再次被使用,比较混乱,比较浪费。

这个弊端也叫作内存碎片化。

3、标记整理法

为了解决标记清除法的弊端,对标记清除法做了升级增强 - 标记整理法。

标记整理法在标记阶段跟标记清除法是一致的,清除阶段会先多一个整理操作,将所有标记需要保留的内存空间整理到前面,将所有标记要清除的内存空间放在后面,然后将后面要清除的空间,释放掉,再将之前的标记都清除。

标记整理法的缺点:移动内存空间,不会立即回收,回收的效率偏低。

4、引用计数清除法

以前的低版本浏览器或现在的个别浏览器,还在使用一种垃圾回收方式叫引用清除法。英文名称(reference counting)。这种清除法是在js引擎中存储了一个表格,内部保存了各个数据被引用的次数。如果一个数据的引用次数为0的时候,表示这个数据就不用了,因此这个内存空间就会被释放掉。

例如:全局变量一直在内引用,所以一直没有被销毁;普通的局部变量,在函数执行结束后,就不再使用,这时候局部的变量的引用计数就是0,就会被销毁,除非还有其他能使用的变量对这个变量保持了引用关系。

例:

var c = new Object() // 计数1
var d = c // 计数2
c = null // 计数1
d = 666 // 计数0 - 所以new Object()的数据空间会被清除掉

这种清除方式容易造成内存泄漏,例如,互相引用:

var a = new Object({name: '张三'}) // 计数1
var b = new Object({name: '李四'}) // 计数1
a.obj = b // 张三对象的数据 计数2
b.obj = a // 张三和李四的数据计数 无限死循环 - 无法被清除了

闭包的空间也是清除不掉的:

function fn() {var a = 1function fun() {console.log(++a)}return fun
}var ff = fn() // 全局ff跟局部的fun保持了引用关系,所以局部的fun不能被销毁。

这种清除方式还有一个弊端,就是必须在内存单独创建一个空间,用来保存统计计数的表格。

5、性能优化

通过学习垃圾回收机制,我们在写代码时,为了提高性能,可以得出以下几点结论:

  1. 避免使用全局变量

    全局变量

  2. 减少判断的层级

  3. 减少数据的读取次数

  4. 减少循环中的计算

  5. 事件绑定优化

  6. 闭包要销毁