JavaScript设计模式笔记-闭包和高阶函数

3.1 闭包

3.1变量作用域

变量作用域指的是变量的有效范围.
JavaScript中是用函数来创建作用域的.在函数中声明一个变量没有带上关键词var那么这个变量会成为全局变量.带上var的情况下这个变量则会成为局部变量

1
2
3
4
5
6
7
8
9
10
11
12
13
var a = 1;
var func1 = function(){
var b = 2;
var func2 = function(){
var c = 3;
alert ( b ); // 输出:2
alert ( a ); // 输出:1
}
func2();
alert ( c ); // 输出:Uncaught ReferenceError: c is not defined
};
func1();

3.2变量的生存周期

对于全局变量来说,全局变量的生命周期是永久的,但是对于函数的局部变量来说,当这个函数被执行完毕后,局部变量也随之销毁.比如3.1的例子.
那么闭包就不同了,看下面一段例子

1
2
3
4
5
6
7
8
9
10
11
var func=function(){
var a=1;
return function(){
a++;
console.log(a)
}
}
var f=func();
f();//2
f();//3
f();//4

上面就是一个简单的闭包,当func被执行完毕后,变量a没有销毁.因为func执行完毕后返回了一个匿名函数.它可以访问到func被调用时产生的环境.这样局部变量的生命周期就被延续了.
比如最常见的例子就是点击一个div弹出div的下标

1
2
3
4
5
6
7
8
9
10
11
12
13
html
body
<div>1</div>
<div>2</div>
<div>3</div>
<div>4</div>
------
var nodes=document.getElementsByTagName('div');
for(var i=0,len=nodes.length;i<len;i++){
nodes[i].onclick=function(){
console.log(i)
}
}

这段代码执行完毕后,你发现你点击div并不会弹出1,2,3,4而是只弹出一个4.因为当你点击的时候也就是事件被触发的时候,for循环早已经结束.那么利用闭包就可以这样修改

1
2
3
4
5
6
7
for(var i=0,len=nodes.length;i<len;i++){
(function(i){
nodes[i].onclick=function(){
console.log(i)
}
})(i)
}

我们也可以利用闭包做一个判断类型的函数

1
2
3
4
5
6
7
8
9
10
11
12
13
var Type={};
//定义需要判断的类型,string,array,number
for(var i=0,type;type=['String','Array','Number'][i++]){
(function(type){
Type['is'+type]=function(obj){
//利用object.toString来判断构造器也就是function是否等于当前的值
return Object.prototype.toString.call(obj)==='[object'+type+']'
}
})(type)
}
Type.isArray([]);//true
Type.isString('str');//true

这段代码在for循环的时候会返回一个函数也就是闭包,把类型判断的值存储进去.就不用每次重写函数了.

3.1.3闭包的更多作用

1.封装变量

闭包可以把一些不需要暴露在全局的变量封装成’私有变量’
利用闭包的特性.下面来编写和提炼一个计算乘积的函数.

1
2
3
4
5
6
7
var mult=function(){
var a=1;
for(var i=0;l=arguments.length;i<l;l++){
a=a*arguments[i];
}
return a;
}

但是这样每次都需要计算,会造成比较大的性能开销,为什么不加入一个缓存机制呢?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var cache={};
var mult=function(){
var arg=[].join.call(arguments)
if(cache[arg]){
return cache[arg];
}
var a=1;
for(var i=0;l=arguments.length;i<l;l++){
a=a*arguments[i];
}
cache[arg]=a;
return cache[arg];
}
console.log(mult(1,2,3));//6

但是上述的代码有个问题也就是cache暴露在全局.这样如果在代码某个地方修改cache那么就会影响到这个cache.于是我们进一步改进,把cache放在函数内部.这时候就可以利用闭包来实现了.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
var mult=function(){
var cache={};
return function(){
var arg=[].join.call(arguments)
if(cache[arg]){
return cache[arg];
}
var a=1;
for(var i=0;l=arguments.length;i<l;l++){
a=a*arguments[i];
}
cache[arg]=a;
return cache[arg];
}()
}
var sum=mult()
sum(1,2,3);//6

进一步提炼.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
//最好是把它们用闭包封闭起来。代码如下:
var mult = (function(){
var cache = {};
var calculate = function(){ // 封闭calculate 函数
var a = 1;
for ( var i = 0, l = arguments.length; i < l; i++ ){
a = a * arguments[i];
}
return a;
};
return function(){
var args = Array.prototype.join.call( arguments, ',' );
if ( args in cache ){
return cache[ args ];
}
return cache[ args ] = calculate.apply( null, arguments );
}
})();

2.延续局部变量的寿命

img对象经常用来数据上报

1
2
3
4
5
var report=function(src){
var img=new Image();
img.src=src
}
report("http://xxx.xxx/getUserInfo")

但是因为低版本浏览器中实现的一些bug导致不是每次report都会成功,原因就是在没有发起HTTP请求之前,img对象就被销毁了.于是我们可以利用闭包的特性来延续局部变量的寿命

1
2
3
4
5
6
7
8
var report=function(){
var imgs=[]
return function(src){
var img=new Image();
img.src=src
imgs.push(img)
}
}

3.闭包和面向对象设计

简而言之 面向对象可以说是过程与数据的结合. 对象以方法的形式包含了过程. 闭包则是在过程中以环境的形式包含了数据 通常OOP(面向对象)可以用闭包实现 反之亦然. 如下例利用闭包实现

1
2
3
4
5
6
7
8
9
10
11
12
var extent=function(){
var value=0;
return {
call:function(){
value++;
console.log(value)
}
}
}
var extent=extent();
extent.call();//1
extent.call();//2

如果换成面向对象的写法就是

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
var extent={
value:0,
call:function(){
this.value++;
console.log(this.value)
}
};
或者
var Extent=function(){
this.value=0
}
Extent.prototype.call=function(){
this.value++;
console.log(this.value)
}
var extent=new Extent();
extent.call();//1

3.1.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
<!DOCTYPE html>
<html>
<body>
<button id='execute'>点我执行命令</button>
<button id='undo'>点我执行命令</button>
</body>
</html>
-----
var Tv={
open:function(){
console.log('打开电视')
},
close:function(){
console.log('关闭电视')
}
}
var OpenTvCommand=function(receiver){
this.receiver=receiver
}
OpenTvCommand.prototype.execute=function(){
this.receiver.open();//执行命令打开电视
}
OpenTvCommand.prototype.undo=function(){
this.receiver.close();//执行撤销命令 关闭电视
}
var setCommand=function(command){
document.getElementById('execute').onclick=function(){
command.execute();//输出 打开电视机
}
document.getElementById('undo').onclick=function(){
command.undo();//输出 关闭电视机
}
}
setCommand(new OpenTvCommand(Tv))

命令模式的意图是把请求封装成对象,从而分离请求的发起者和请求的执行者之间的耦合关系.在命令被执行之前可以预先往命令对象中植入命令的接受者.
就拿上例来说.发起者是DOM对象也就是button标签.接受者也就是命令是OpenTvCommand
在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
25
26
27
28
29
var Tv={
open:function(){
console.log('打开电视')
},
close:function(){
console.log('关闭电视')
}
}
var createCommand=function(receiver){
var execute=function(){
return receiver.open();//执行命令 打开电视机
}
var undo=function(){
return receiver.close();//执行命令 关闭电视机
}
return {
execute:execute
undo:undo
}
}
var setCommand=function(command){
document.getElementById('execute').onclick=function(){
command.execute();//输出 打开电视机
}
document.getElementById('undo').onclick=function(){
command.undo();//输出 关闭电视机
}
}
setCommand(createCommand(Tv))

3.1.6 闭包与内存管理

因为闭包的原因,局部变量不会销毁,这样会造成内存开销.
如果闭包作用域链保存着一些DOM节点,这时候就有可能造成内存泄露.
要解决的办法也很简单 手动把这些变量设置为null

3.2 高阶函数

高阶函数是指至少满足下例条件之一的函数

  • 函数可以作为参数被传递
  • 函数可以作为返回值输出

3.2.1 函数作为参数传递

1.回调函数

最常见的例子就是ajax.比如待请求完毕后执行callback函数

1
2
3
4
5
6
7
8
9
10
var getUserInfo=function(userId,callback){
$.ajax('http://xxx.com/getUserInfo?'+userId,function(data){
if(typeof callback==='function'){
callback(data)
}
})
}
getUserInfo(13157,function(data){
console.log(data)
})

回调函数不仅仅是应用在异步请求中,比如当一个函数不适合执行一些请求时,我们也可以把这些请求委托给另一个函数来处理.比如我要创建100个div

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//于是我们把div.style.display = 'none'这行代码抽出来,用回调函数的形式传入appendDiv方法:
var appendDiv = function( callback ){
for ( var i = 0; i < 100; i++ ){
var div = document.createElement( 'div' );
div.innerHTML = i;
document.body.appendChild( div );
if ( typeof callback === 'function' ){
callback( div );
}
}
};
appendDiv(function( node ){
node.style.display = 'none';
});

2.Array.prototype.sort

数组排序的函数.从这个函数使用可以看到,我们的目的是对数组进行排序,这个是不变的部分.用什么规则去排序是可变的部分,把可变的部分抽出来封装在函数中

1
2
3
4
5
//从小到大排列
[1,4,3].sort(function(a,b){
return a-b
})
.......

3.2.2 函数作为返回值输出

1.判断数据的类型

这个我们在闭包那节已经写过了,利用函数当做返回值的特性批量注册判断数据函数,如果单纯的写每个类型都需要写一个函数

1
2
3
4
5
6
7
8
9
10
11
12
13
var Type={};
//定义需要判断的类型,string,array,number
for(var i=0,type;type=['String','Array','Number'][i++]){
(function(type){
Type['is'+type]=function(obj){
//利用object.toString来判断构造器也就是function是否等于当前的值
return Object.prototype.toString.call(obj)==='[object'+type+']'
}
})(type)
}
Type.isArray([]);//true
Type.isString('str');//true

2.getSingle

单例模式

1
2
3
4
5
6
var getSingle=function(fn){
var ret;
return function(){
return ret||(ret=fn.apply(this,arguments))
}
}

这个例子传递了一个函数同时返回了另一个函数,效果如下

1
2
3
4
5
6
var getScript=getSingle(function){
return document.createElement('script')
}
var script1=getScript()
var script2=getScript()
console.log(script1===script2);//true

这个单例模式很好理解,利用闭包的特性存储函数,如果ret已经存在也就是被存储的话就返回当前的ret不返回和更新新的ret

3.2.3 高阶函数实现AOP

AOP(面向切片编程)主要作用就是把跟一些核心业务逻辑无关的功能抽离出来,如 日志统计,安全控制,异常处理等
在JavaScript中实现AOP如下

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
Function.prototype.before=function(beforeFn){
var __self=this;
console.log('before',this)
return function(){
var ret=__self.apply(this,arguments)
beforeFn.apply(this,arguments)
console.log('before ret',ret)
return ret
}
}
Function.prototype.after=function(afterFn){
var __self=this;
console.log('after',this)
return function(){
var ret=__self.apply(this,arguments)
afterFn.apply(this,arguments)
console.log('after ret',ret)
return ret
}
}
var func=function(){
console.log(2);
}
func=func.before(function(){
console.log(1);
}).after(function(){
console.log(3)
})
func()

我们把上述的打印数字1和3动态植入func函数.那么来分析下这段函数
首先定义func的时候会执行before函数 before会先保存当前的函数也就是打印数字2的函数,同时先执行before这个函数的内容也就是打印数字1
执行完毕后再执行after函数.跟before函数一样的功能..先执行一下before函数再执行当前的after函数.再返回undefined
这段代码的意思可以用下例来解释

1
2
3
4
5
6
7
8
9
10
11
12
var func1=function(){
console.log(1)
}
var func2=function(){
console.log(2)
func1()
}
var func=function(){
func2()
console.log(3)
}
func2()

3.2.4高阶函数的其他应用

1.函数柯里化

比如一个计算开销的函数

1
2
3
4
5
6
7
var monthlyCost=0;
var cost=function(money){
monthlyCost+=money
}
cost(100);//第一天开销
cost(200);//第二天开销
console.log(monthlyCost);//两天总开销

但是可以利用闭包的特性来保存每次传递的参数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
var cost = (function(){
var args = [];
return function(){
if ( arguments.length === 0 ){
var money = 0;
for ( var i = 0, l = args.length; i < l; i++ ){
money += args[ i ];
}
return money;
}else{
[].push.apply( args, arguments );
}
}
})();
cost( 100 ); // 未真正求值
cost( 200 ); // 未真正求值
cost( 300 ); // 未真正求值
console.log( cost() ); // 求值并输出:600

如果检测到未传递值的情况也就是arguments为空的情况则认定为求值,其他的情况则把值存储起来
下面来编写一个真正的柯里化函数

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
var currying = function( fn ){
var args = [];
return function(){
if ( arguments.length === 0 ){
return fn.apply( this, args );
}else{
[].push.apply( args, arguments );
return arguments.callee;
}
}
};
var cost = (function(){
var money = 0;
return function(){
for ( var i = 0, l = arguments.length; i < l; i++ ){
money += arguments[ i ];
}
return money;
}
})();
var cost = currying( cost ); // 转化成currying 函数
cost( 100 ); // 未真正求值
cost( 200 ); // 未真正求值
cost( 300 ); // 未真正求值
alert ( cost() ); // 求值并输出:600

2.反柯里化

先来看看一段通用的反柯里化实现函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
Function.prototype.uncurrying=function(){
var self=this;
return function(){
var obj=Array.prototype.shift.call(arguments);
return self.apply(obj,arguments)
}
}
作用
————
var push=Array.prototype.push.uncurrying();
(function(){
push(arguments,4)
console.log(arguments);//[1,2,3,4]
})(1,2,3)

push函数反柯里化后返回的是一个函数.obj存储了执行环境的上下文.
那么也可以批量反柯里化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
for(var i=0,fn,arr=['push','shift','forEach'];fn=arr[i++]){
Array[fn]=Array.prototype[fn].uncurrying()
}
var obj={
'length':3,
'0':1,
'1':2,
'3':3
}
Array.push( obj, 4 ); // 向对象中添加一个元素
console.log( obj.length ); // 输出:4
var first = Array.shift( obj ); // 截取第一个元素
console.log( first ); // 输出:1
console.log( obj ); // 输出:{0: 2, 1: 3, 2: 4, length: 3}
Array.forEach( obj, function( i, n ){
console.log( n ); // 分别输出:0, 1, 2
});

从这里可以得知反柯里化就是去借用一系列的方法,从而使没有该方法的函数可以使用该方法 举个例子 五段斩是剑圣的技能 反柯里化后 五段斩独立出来了 剑圣可以用 鬼泣也可以用.

3.函数节流

JavaScript函数大多数都是由用户主动触发的,但是别忘了事件这个概念 浏览器窗口不断的变化会引发事件函数这样会造成特别大的开销 比如 window.onresize事件,下面给出一种节流函数的实现 实际上就是延迟触发函数

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 throttle=function(fn,interval){
var _self=fn,//保存需要被延迟函数的引用
timer,//定时器
firstTime=true;//是否第一个调用
return function(){
var args=arguments,
_me=this;
if(firstTime){
__self.apply(__me,args)
return firstTime=false
}
if(timer){
return false
}
timer=setTimeout(function(){
clearTimeout(timer);
timer=null;
__self.apply(__me,args)
},interval||500)
}
}
window.onresize=throttle(function(){
console.log(1)
},500)

首先这个函数会返回一个函数.也就是闭包.闭包返回的函数会先判断定时器是否存在和是否是第一次调用如果是第一次调用那么就立即执行不需要等待500ms
这里用的是interval 也就是每500ms调用一下这个函数

4.分时函数

比如说需要添加1000个DOM节点

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var ary = [];
for ( var i = 1; i <= 1000; i++ ){
ary.push( i ); // 假设ary 装载了1000 个好友的数据
};
var renderFriendList = function( data ){
for ( var i = 0, l = data.length; i < l; i++ ){
var div = document.createElement( 'div' );
div.innerHTML = i;
document.body.appendChild( div );
}
};
renderFriendList( ary );

这样一加载浏览器绝对GG那么用分时函数就可以避免这个问题了

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
var timeChunk=function(ary,fn,count){
var obj,
t;
var len=ary.length;
var start=function(){
//返回1个或者指定数字的最小值
for(var i=0;i<Math.min(count||1,ary.length);i++){
var obj=ary.shift();//每次弹出一个
fn(obj)
}
};
return function(){
t=setInterval(function(){
if(ary.length===0){//如果所有节点都被创建好 那么就清除定时器
return clearInterval(t)
}
start()
},200)//分批执行的时间间隔,也可以用参数的形式传入
}
}
------
var ary = [];
for ( var i = 1; i <= 1000; i++ ){
ary.push( i );
};
var renderFriendList = timeChunk( ary, function( n ){
var div = document.createElement( 'div' );
div.innerHTML = n;
document.body.appendChild( div );
}, 8 );
renderFriendList();

上述的分时函数挺好理解的 每200ms执行一次start函数,start函数则会从ary中每次获取8个节点 然后创建.

### 5.惰性加载函数
比如要实现浏览器的绑定兼容事件函数 第一种写法的为

1
2
3
4
5
6
7
8
var addEvent=function(elem,type,handler){
if(window.addEventLister){
return elem.addEventLister(type,handler,false)
}
if(window.attachEvent){
return elem.attachEvent('on'+type,handler)
}
}

但是每次执行这个addEvent的时候都会要执行里面的if分支,那么是否有更简单的办法呢?
第二种方案

1
2
3
4
5
6
7
8
9
10
11
12
var addEvent = (function(){
if ( window.addEventListener ){
return function( elem, type, handler ){
elem.addEventListener( type, handler, false );
}
}
if ( window.attachEvent ){
return function( elem, type, handler ){
elem.attachEvent( 'on' + type, handler );
}
}
})();

但是如果我们没有使用过addEvent函数也会造成开销
第三种方案也就是惰性加载方案

1
2
3
4
5
6
7
8
9
10
11
12
var addEvent = function( elem, type, handler ){
if ( window.addEventListener ){
addEvent = function( elem, type, handler ){
elem.addEventListener( type, handler, false );
}
}else if ( window.attachEvent ){
addEvent = function( elem, type, handler ){
elem.attachEvent( 'on' + type, handler );
}
}
addEvent( elem, type, handler );
};

这个函数在第一调用的时候会先判断下 然后直接覆盖了addEvent函数 以后每次调用就不用if判断了