设计模式和垃圾回收 一、设计模式 1、设计模式介绍 设计模式是面向对象 中反复多次出现的问题,而总结出来的最优解决方案。
例如:我们使用字面量方式创建对象,容易造成重复,这个在应用过程中是不可避免的,为了解决这个问题,我们通过一个函数来解决重复,也就是工厂函数。此时就可以把这套方案叫做工厂模式。
不同的问题,总结出的方案也是不同的。在当前的实际应用过程中,人们总结了23种设计模式。常见的设计模式有:
单例模式 组合模式 观察者模式 发布订阅模式 命令模式 代理模式 工厂模式 策略模式 适配器模式 2、单例模式 单例模式是指,通过一个类衍生出的对象,只能有一个,不会有第二个,就算有第二个,跟第一个是共享同一个堆地址的。
单例模式解决的问题是,当我们多次调用一个类中方法的时候,通常需要实例化多次,然后调用方法。但其实每次使用的方法有一个类就已经能用了,实例化多次,得到的多个实例化对象,造成了内存浪费,所以需要单例模式:
将一个类放在全局,不可避免的,就会被实例化多次,所以,放在全局不能实现单例模式。
1 2 3 4 5 function fn ( ) { class Single {} var s = new Single () return s }
放在局部,每次调用函数,会重新实例化类得到新的s变量,跟放在全局的效果是一样的。如何确保多次调用函数,在使用同一个s变量 - 闭包:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 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)
此时创建对象只能通过调用fn中的返回函数,且多次调用使用的是同一个数据s。
但fn在全局是可以被调用多次的,每次调用都会创建一个独立的不会销毁的执行空间,也就是会得到多个不同的小函数,调用不同的小函数得到的对象也是独属于不销毁的执行空间的,还是会有不同:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 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)
为了保证fn只能被调用一次,将fn作为自调用函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 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)
此时创建对象只能调用fun,且每次调用共用同一个s变量,这样就形成了最终的单例模式。
3、组合模式 组合模式指的是,每个面向对象效果,在开启的时候,都需要先实例化对象,然后调用实例化对象初始化方法开启。有多个实例对象,我们就需要实例化多次并调用多次初始化方法。
如果将初始化方法比作是一个开灯的开关,我们就可以将多个开关组成一个组合开关,只要开启组合开关就让所有开关都开启。组合模式就是要制作这样一个组合开关。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 class Carousel { init ( ) { console .log ('这是轮播图的初始化方法' ) } } class Tab { init ( ) { console .log ('这是tab切换的初始化方法' ) } } class Enlarge { init ( ) { console .log ('这是放大镜的初始化方法' ) } }
如何将这3个效果的初始化方法调用作为一个组合开关?
思路:既然要组合,就需要将这3个单独的开关组合在一起。将这个3个类的实例化对象,都放在同一个容器中,遍历容器中的每个效果对象,调用初始化方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 class Combination { 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,如何更加灵活的做组合开关呢?
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 class Carousel { init ( ) { console .log ('这是轮播图的初始化方法' ) } } class Tab { start ( ) { console .log ('这是tab切换的初始化方法' ) } } class Enlarge { go ( ) { console .log ('这是放大镜的初始化方法' ) } } class Combination { 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.发布订阅模式 发布订阅模式分为两个概念,一个是发布者,一个订阅者。当发布者发布消息后,订阅过这个消息的订阅者能接收到这个消息并执行一定的操作。
例如:小明到商店买煊赫门香烟,但是煊赫门卖完了,小明就在商店进行登记,当商店有货的时候,通知小明,小明就可以去买了。
发布者:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 class Publisher { subscribers = [] addSub (...arr ) { this .subscribers .push (...arr) } notify (message ) { this .subscribers .forEach (item => { if (item.message === message) { item.execute () } }) } }
订阅者:
1 2 3 4 5 6 7 8 class Subscriber { constructor (message, cb ) { this .message = message this .execute = cb } }
使用示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 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、观察者模式 观察者模式的含义,是一个观察者在观察某件事件的进展,当进行到一定程度的时候就会执行某个操作。
例如:我要做核算,但是做核算的人比较多,我就不停的观察,当排队的人少的时候,我就去做。
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 class Watcher { container = {} bind (type, handler ) { if (!this .container [type]) { this .container [type] = [] } this .container [type].push (handler) } touch (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] } }
使用示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 var w = new Wathcer ()w.bind ('a1' , fa1) function fa1 ( ) { consolel.log ('时机到了,该执行了1' ) } w.bind ('a1' , fa2) function fa2 ( ) { consolel.log ('时机到了,该执行了2' ) } w.touch ('a1' ) w.unbind ('a1' , fa1) w.touch ('a1' ) w.clear () w.touch ('a1' )
二、垃圾回收 1、垃圾回收介绍 任何代码的执行,都需要在内存中进行,代码运行结束后,就需要释放内存,否则随着代码运行的数量增多,有限的内存会承受不住太大的负担而崩溃。
浏览器为了释放js中没有用的内存空间,设计了专业的垃圾回收机制。
垃圾回收机制,不是即时处理垃圾内存的,而是有周期性的,隔一段时间,处理一次。
被内存空间中,页面在打开状态时,全局变量的内存是不会被释放的,因为在页面正在运行时,全局变量随时都可能会使用。全局变量的内存会在页面关闭后被回收。
所以这里要介绍的垃圾回收机制,主要是指局部变量和执行空间。
js的垃圾回收机制,全称(Garbage Collecation),简称GC
2、标记清除法 现如今大部分浏览器所使用的垃圾回收机制。
标记清除回收机制的原理:
对所有数据的内存空间遍历,对所有能访问到的数据的内存空间做标记 遍历所有数据的内存空间,将没有做标记的数据空间释放,并还原第1步操作 例:
1 2 3 4 5 6 7 var a = 1 var b = aa = null var c = new Object ()var d = cc = null
将a赋值为null,数字1在内存中占用的空间会在下次回收垃圾的时候回收掉。c赋值为null,原本new出来的实例对象所在内存空间还在被变量d使用,所有不会被回收。
这种回收机制的缺点,就是在清除无用的空间后,有数据的空间是不连续的,造成某些内存空间无法再次被使用,比较混乱,比较浪费。
这个弊端也叫作内存碎片化。
3、标记整理法 为了解决标记清除法的弊端,对标记清除法做了升级增强 - 标记整理法。
标记整理法在标记阶段跟标记清除法是一致的,清除阶段会先多一个整理操作,将所有标记需要保留的内存空间整理到前面,将所有标记要清除的内存空间放在后面,然后将后面要清除的空间,释放掉,再将之前的标记都清除。
标记整理法的缺点:移动内存空间,不会立即回收,回收的效率偏低。
4、引用计数清除法 以前的低版本浏览器或现在的个别浏览器,还在使用一种垃圾回收方式叫引用清除法。英文名称(reference counting)。这种清除法是在js引擎中存储了一个表格,内部保存了各个数据被引用的次数。如果一个数据的引用次数为0的时候,表示这个数据就不用了,因此这个内存空间就会被释放掉。
例如:全局变量一直在内引用,所以一直没有被销毁;普通的局部变量,在函数执行结束后,就不再使用,这时候局部的变量的引用计数就是0,就会被销毁,除非还有其他能使用的变量对这个变量保持了引用关系。
例:
1 2 3 4 var c = new Object () var d = c c = null d = 666
这种清除方式容易造成内存泄漏,例如,互相引用:
1 2 3 4 var a = new Object ({name : '张三' }) var b = new Object ({name : '李四' }) a.obj = b b.obj = a
闭包的空间也是清除不掉的:
1 2 3 4 5 6 7 8 9 function fn ( ) { var a = 1 function fun ( ) { console .log (++a) } return fun } var ff = fn ()
这种清除方式还有一个弊端,就是必须在内存单独创建一个空间,用来保存统计计数的表格。
5、性能优化 通过学习垃圾回收机制,我们在写代码时,为了提高性能,可以得出以下几点结论:
避免使用全局变量
全局变量
减少判断的层级
减少数据的读取次数
减少循环中的计算
事件绑定优化
闭包要销毁