本文进行迭代器模式和发布订阅模式的总结。这两个模式算是比较经典的模式,甚至已经到了语法本身支持的程度。
迭代器模式 定义迭代器模式是指提供一种方法顺序访问一个聚合对象 中的各个元素,而又不需要暴露该对象的内部表示。迭代器模式可以把迭代的过程从业务逻辑中分离出来,在使用迭代器模式之后,即使不关心对象的内部构造,也可以按顺序访问其中的每个元素。
比如我们定义一个数据结构,内部是用链表构造的一个队列,只需要对外提供一个方法可以顺序遍历这个队列就行,并不需要让外部了解到这个队列用的是怎样的技术去实现的。
内部迭代器和外部迭代器我们来实现一个迭代器each函数,接受两个参数,第一个位被循环的数组,第二个为循环中每一步后将被触发的回调函数:
1 2 3 4 5 6 7 8 9 var each = function (arr, callback ) { for (var i = 0 , l = arr.length ; i < l; i++) { callback.call (arr[i], i, arr[i]) } } each ([1 ,2 ,3 ], function (index, n) ) { console .log (index, n); }
这个each函数就属于内部迭代器,each函数的内部已经定义好了迭代规则,完全接手整个迭代过程,外部只需要一次初始调用。
内部迭代器在调用的时候非常方便,外界不关心迭代器内部的实现,跟迭代器的交互仅是一次初始调用,但这也正好是内部迭代器的缺点。
由于内部迭代器的迭代规则已经被提前定义,上面的each函数就无法同时迭代两个数组。
比如现在有个需求,比较两个数组的元素是否完全相同,如果不改写each函数,我们能够入手的地方似乎只剩下each的回调函数了,代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 var compare = function (arr1, arr2 ) { if (arr1.length !== arr2.length ) { throw new Error ("arr1 and arr2 is not equal" ) } each (arr1, function (i, n ) { if (n !== arr2[i]) { throw new Error ("arr1 and arr2 is not equal" ) } }) alert ("arr1 and arr2 is equal" ) } compare ([1 ,2 ,3 ],[1 ,2 ,4 ])
在一些没有闭包的语言中,内部迭代器本身的实现也相当复杂。比如C语言中的内部迭代器,利用函数指针实现的,循环处理的函数都要通过参数的形式明确从外面传递进去。
相比于内部迭代器,外部迭代器则是必须显式地请求迭代下一个元素。
外部迭代器增加了一些调用的复杂度,但是也增加了迭代器的灵活性,我们可以手工控制迭代的过程和顺序。
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 var Iterator = function (obj ) { var current = 0 ; var next = function ( ) { current++; } var isDone = function ( ) { current >= obj.length ; } var getCurrent = function ( ) { return obj[current]; } return { next, isDone, getCurrent, } } var compare = function (iterator1, iterator2 ) { while (!iterator1.isDone () && !iterator2.isDone ()) { if (iterator1.getCurrent () !== iterator2.getCurrent ()) { throw new Error ("iterator1 and iterator2 is not equal" ) } iterator1.next (); iterator2.next (); } if (iterator1.isDone () && iterator2.isDone ()) { alert ('iterator1 and iterator2 is equal' ); } else { throw new Error ('iterator1 and iterator2 is not equal' ); } } compare (Iterator ([1 ,2 ,3 ]), Iterator ([1 ,2 ,3 ]));
中止迭代器迭代器可以像普通for循环中的break一样,提供一种跳出循环的方法。如:
1 2 3 4 5 6 7 8 9 10 11 12 var each = function (arr, callback ) { for (var i = 0 , l = arr.length ; i < l; i++) { if (callback.call (arr[i], i, arr[i]) === false ) { break ; } } } each ([1 ,2 ,3 ,4 ,5 ,6 ], function (i, n ) { if (n > 3 ) return false ; console .log (n); })
迭代器模式举例假设有如下代码,可以根据不同的浏览器获取不同的上传组件对象:
1 2 3 4 5 6 7 8 9 10 11 12 13 var getUploadObj = function ( ) { try { return new ActiveXObject ("TXFINActiveX.FTNUpload" ); } catch (e) { if (supportFlash ()) { var str = "<object type='application/x-shockwave-flash'></object>" return $(str).appendTo ($('body' )); } else { var str = "<input name='file' type='file'/>" return $(str).appendTo ($('body' )) } } }
我们用迭代器模式改造一下上面那段代码,将每种获取上传组件的方法封装为一个函数,然后见这些函数放入一个数组中去迭代,直到有一个可以正确返回。
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 var iteratorUploadObj = function ( ) { const uploadObjCreators = Array .prototype .slice (arguments ); for (var i = 0 ; i < uploadObjCreators.length ; i++) { var uploadObj = uploadObjCreators[i](); if (uploadObj !== false ) { return uploadObj; } } } var getActiveUploadObj = function ( ) { try { return new ActiveXObject ("TXFINActiveX.FTNUpload" ); } catch (e) { return false ; } } var getFlashUploadObj = function ( ) { if (supportFlash ()) { var str = "<object type='application/x-shockwave-flash'></object>" return $(str).appendTo ($('body' )); } else { return false ; } } var getFormUploadObj = function ( ) { var str = "<input name='file' type='file'/>" return $(str).appendTo ($('body' )) } var uploadObj = iteratorUploadObj (getActiveUploadObj, getFlashUploadObj, getFormUploadObj);
发布订阅模式发布订阅模式又叫做观察者模式,它定义对象之间的一种一对多的依赖关系,当一个对象的状态发生改变的时候,所有依赖它的对象都会得到通知。
在JavaScript中,我们一般用事件模式来代替发布订阅模式。
发布订阅模式可以广泛应用于异步编程中,这是一种替代传递回调函数的方案。同时,发布订阅模式可以取代对象之间硬编码的通知机制,一个对象不再显式地调用另外一个对象的某个接口。发布订阅模式让两个对象松耦合地联系在一起。
DOM事件实际上,只要我们曾经在DOM节点上绑定过事件函数,那我们就曾经使用过发布订阅模式:
1 2 3 4 5 document .body .addEventListener ('click' , function ( ) { alert (2 ); }, false ) document .body .click ();
自定义事件除了DOM事件,我们经常还会实现一些自定义事件,这种依靠自定义事件完成的发布订阅模式可以用于任何JavaScript代码中。
我们看看如何一步步实现发布订阅模式
首先要指定好谁充当发布者 然后给发布者添加一个缓存列表,用于存放回调函数以便通知订阅者 最后发布消息时候,发布者会遍历这个缓存列表,依次触发里面存放的订阅者回调函数 另外,我们还可以往回调函数里填入一些参数,订阅者可以接收这些参数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 var salesOffices = {}; salesOffices.clientList = []; salesOffices.listen = function (fn ) { this .clientList .push (fn); } salesOffices.trigger = function ( ) { for (const fn of this .clientList ) { fn.apply (this , arguments ) } } salesOffices.listen (function (price, squareMeter ) { console .log ("A know:" , price, squareMeter); }) salesOffices.listen (function (price, squareMeter ) { console .log ("B know:" , price, squareMeter); }) salesOffices.trigger (2000000 , 80 ); salesOffices.trigger (3000000 , 110 );
至此,我们实现了一个简单的发布订阅模式,但这里还存在一些问题。我们看到订阅者接收到了发布者发布的每个消息,虽然A只想买88平方米的房子,但是发布者会把110房子的消息也推送给A,这是没有必要的,所以我们可以优化一下:
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 var salesOffices = {}; salesOffices.clientList = {}; salesOffices.listen = function (key, fn ) { if (!this .clientList [key]) { this .clentList [key] = []; } this .clentList [key].push (fn); } salesOffices.trigger = function ( ) { const key = Array .prototype .shift .call (arguments ); const fns = this .clentList [key]; if (!fns || fns.length === 0 ) { return false ; } for (const fn of fns) { fn.apply (this , arguments ) } } salesOffices.listen ('squareMeter80' , function (price ) { console .log ("A know:" , price); }) salesOffices.listen ('squareMeter110' , function (price ) { console .log ("B know:" , price); }) salesOffices.trigger ('squareMeter80' , 2000000 ); salesOffices.trigger ('squareMeter110' , 3000000 );
发布订阅的通用实现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 var event = { clientList : {}, listen : function (key, fn ) { if (!this .clientList [key]) { this .clentList [key] = []; } this .clentList [key].push (fn); }, trigger : function ( ) { const key = Array .prototype .shift .call (arguments ); const fns = this .clentList [key]; if (!fns || fns.length === 0 ) { return false ; } for (const fn of fns) { fn.apply (this , arguments ) } } } var installEvent = function (obj ) { for (var i in event) { obj[i] = event[i]; } }
此时我们可以
1 2 3 4 5 6 7 8 9 10 11 12 13 14 var salesOffices = {}; installEvent (salesOffices);salesOffices.listen ('squareMeter80' , function (price ) { console .log ("A know:" , price); }) salesOffices.listen ('squareMeter110' , function (price ) { console .log ("B know:" , price); }) salesOffices.trigger ('squareMeter80' , 2000000 ); salesOffices.trigger ('squareMeter110' , 3000000 );
当然,我们也可以先发布,再订阅,当暂时没有订阅者时将消息缓存起来,一旦出现订阅者再依次调用订阅者并清空消息