设计模式和垃圾回收
一、设计模式
1、设计模式介绍
设计模式是面向对象中反复多次出现的问题,而总结出来的最优解决方案。
例如:我们使用字面量方式创建对象,容易造成重复,这个在应用过程中是不可避免的,为了解决这个问题,我们通过一个函数来解决重复,也就是工厂函数。此时就可以把这套方案叫做工厂模式。
不同的问题,总结出的方案也是不同的。在当前的实际应用过程中,人们总结了23种设计模式。常见的设计模式有:
-
单例模式
-
组合模式
-
观察者模式
-
发布订阅模式
-
命令模式
-
代理模式
-
工厂模式
-
策略模式
-
适配器模式
2、单例模式
单例模式是指,通过一个类衍生出的对象,只能有一个,不会有第二个,就算有第二个,跟第一个是共享同一个堆地址的。
单例模式解决的问题是,当我们多次调用一个类中方法的时候,通常需要实例化多次,然后调用方法。但其实每次使用的方法有一个类就已经能用了,实例化多次,得到的多个实例化对象,造成了内存浪费,所以需要单例模式:
class Single{}
将一个类放在全局,不可避免的,就会被实例化多次,所以,放在全局不能实现单例模式。
function fn() { class Single{} var s = new Single() return s }
放在局部,每次调用函数,会重新实例化类得到新的s变量,跟放在全局的效果是一样的。如何确保多次调用函数,在使用同一个s变量 - 闭包:
function fn() { class Single{} var s return 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 s return 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 s return 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 = message this.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步操作
例:
var a = 1 var b = a a = null var 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 = 1 function fun() { console.log(++a) } return fun } var ff = fn() // 全局ff跟局部的fun保持了引用关系,所以局部的fun不能被销毁。
这种清除方式还有一个弊端,就是必须在内存单独创建一个空间,用来保存统计计数的表格。
5、性能优化
通过学习垃圾回收机制,我们在写代码时,为了提高性能,可以得出以下几点结论:
-
避免使用全局变量
全局变量生命周期特别长,页面不关闭,他就不销毁。
如何避免:
var换let和const
将所有代码放在自调用函数中
-
减少判断的层级
-
减少数据的读取次数
-
减少循环中的计算
-
事件绑定优化
-
闭包要销毁