有意思的JS

函数闭包&柯里化

闭包

有这么一道题:

要实现一个函数,主要功能是对参数进行加法运算,但是参数传递方式略有不同:

CISDI_Cal(0).toString() //输出0
CISDI_Cal(0)(1).toString() //输出1
CISDI_Cal(0)(1)(2).toString() //输出3
var v = CISDI_Cal(0)(1); v(2).toString() //输出3

这个题考察的是javascript里面常用的函数闭包,从示例可以看到:

  1. 从第二个示例CISDI_Cal(n)返回的是一个函数对象,从第三和四示例可以看到CISDI_Cal(n)(n)返回的也是一个函数对象;
  2. CISDI_Cal(0).toString()返回了累加的结果,说明每个返回的函数对象都有toString方法;
  3. 多次函数调用累加得到结果,故前一函数获得参数累加后的值保留到了后一函数进行累加。

由上面分析可知,CISDI_Cal内部有一个函数,这个函数执行后对输入进行了累加并返回了一个同样的函数,这个函数有toString方法输出累加结果:

1
2
3
4
5
6
7
8
9
10
function CISDI_Cal(n) {
function add(a) {
n += a;
return add;
}
add.toString = function() {
return n;
}
return add;
}

上面这个例子讲的是函数闭包,过上面这种特殊的函数写法,可以让一个函数读取一个与自己不同作用域的局部变量,上面的n是函数CISDI_Cal内的局部变量,对add是可见的,但是反过来就不行,add内部的局部变量,对CISDI_Cal就是不可见的,既然add可以读取CISDI_Cal中的局部变量,那么只要把add作为返回值,我们就可以在CISDI_Cal外部读取它的内部变量,简单一句话来说函数闭包就是 函数内包含子函数,并最终return子函数
闭包函数的最大价值在于:我们可以在函数的外部(即子函数),直接读取该函数的局部变量。再仔细研究,就会发现CISDI_Cal函数就如同一个“类”,而其定义的局部变量就如同该“类”的全局变量;而子函数add函数,则如同这个“类”的方法,可以直接使用这个“类”的全局变量n。

闭包函数的主要作用

  1. 缓存:可以实现数据缓存,我们可以把一个需要长期用到的变量设为闭包函数的局部变量,在子函数里面直接使用它。因此局部变量只定义初始化一次,但我们可以多次调用子函数并使用该变量。这比起我们在子函数中定义初始化变量,多次调用则多次初始化的做法,效率更高。闭包函数常见的一种用途就是,我们可以通过此实现计数功能。在闭包函数定义一个计数变量,而在子函数中对其进行++的操作。这样每次调用闭包函数,计数变量就会加1。
  2. 实现封装:如同前面所说,闭包函数就如同一个“类”,只有在该闭包函数里的方法才可以使用其局部变量,闭包函数之外的方法是不能读取其局部变量的。这就实现了面向对象的封装性,更安全更可靠。

那什么是柯里化呢?

额,这么说吧…利用柯里化机制的函数function就是闭包函数。

柯里化(Currying),又称部分求值(Partial Evaluation),是把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数而且返回结果的新函数的技术。

百科上的定义是针对众多函数式语言而言的,按照Stoyan Stefanov(《JavaScript Pattern》作者)的说法,所谓“柯里化”就是使函数理解并处理部分应用.举个栗子的话,就是下面这个(来自张鑫旭):

柯南身子虽小,但是里面住的却是大柯南,也就是一个function里面还有个function。不同柯南处理不同情况,例如,小柯南可以和…稍等,他女朋友叫什么的忘了,我查查…哦,毛利兰一起洗澡澡;但是大柯南就不行。小柯南不能当面指正犯人,需借助小五郎;但是,大柯南就可以直接质问指出凶手。就类似于,内外function处理不同的参数。如果代码表示就是(小柯南=smallKenan; 大柯南=bigKenan; 小柯南嗑药会变大柯南):

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 smallKenan = function(action) {
var bigKenan = function(doing) {
var result = "";
if (action === "take drugs") {
if (doing === "bathWithGirlFriend") {
result = "尖叫,新一,你这个色狼,然后一巴掌,脸煮熟了~";
} else if (doing === "pointOutKiller") {
result = "新一,这个案子就交给你的,快点找出谁是凶手吧~";
}
} else {
if (doing === "bathWithGirlFriend") {
result = "来吧,柯南,一起洗澡吧~";
} else if (doing === "pointOutKiller") {
result = "小孩子家,滚一边去!";
}
}
console.log(result);
return arguments.callee; // 等同于return bigKenan
};
return bigKenan;
};
// 小柯南吃药了,然后和毛利兰洗澡,凶案现场指证犯人;结果是……
smallKenan("take drugs")("bathWithGirlFriend")("pointOutKiller");

结果如下:

尖叫,新一,你这个色狼,然后一巴掌,脸煮熟了~
新一,这个案子就交给你的,快点找出谁是凶手吧~

“吃药”、“洗澡”、“指出凶手”就可以看成三个参数,其中,“吃药”确实是小柯南使用的,而后面的是“洗澡”、“指出凶手”虽然跟在smallKenan()后面,实际上是大柯南使用的。这个就是柯里化,参数部分使用。外部函数处理部分应用,剩下的由外部函数的返回函数处理。

柯里化有3个常见作用:1. 参数复用;2. 提前返回;3. 延迟计算/运行。

1.参数复用

前面第一个栗子就是,每次add都需要一个n参与计算,并保存计算结果,通过柯里化过程,add无需添加这个多余的参数。

2.提前返回

很常见的一个例子,兼容现代浏览器以及IE浏览器的事件添加方法。我们正常情况可能会这样写:

1
2
3
4
5
6
7
8
9
10
11
var addEvent = function(el, type, fn, capture) {
if (window.addEventListener) {
el.addEventListener(type, function(e) {
fn.call(el, e);
}, capture);
} else if (window.attachEvent) {
el.attachEvent("on" + type, function(e) {
fn.call(el, e);
});
}
};

上面的方法有什么问题呢?很显然,我们每次使用addEvent为元素添加事件的时候,(eg. IE6/IE7)都会走一遍if…else if …,其实只要一次判定就可以了,怎么做?–柯里化。改为下面这样子的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var addEvent = (function(){
if (window.addEventListener) {
return function(el, sType, fn, capture) {
el.addEventListener(sType, function(e) {
fn.call(el, e);
}, (capture));
};
} else if (window.attachEvent) {
return function(el, sType, fn, capture) {
el.attachEvent("on" + sType, function(e) {
fn.call(el, e);
});
};
}
})();

初始addEvent的执行其实值实现了部分的应用(只有一次的if…else if…判定),而剩余的参数应用都是其返回函数实现的,典型的柯里化。

3.延迟计算

一般而言,延迟计算或运行是没有必要的,因为一天花10块钱和月末花300块钱没什么本质区别——只是心里好受点(温水炖青蛙)。嘛,毕竟只是个人看法,您可能会不这么认为。举个例子,我每周末都要去钓鱼,我想知道我12月份4个周末总共钓了几斤鱼,把一些所谓的模式、概念抛开,我们可能就会下面这样实现:

1
2
3
4
5
6
7
8
9
10
11
var fishWeight = 0;
var addWeight = function(weight) {
fishWeight += weight;
};
addWeight(2.3);
addWeight(6.5);
addWeight(1.2);
addWeight(2.5);
console.log(fishWeight); // 12.5

每次addWeight都会累加鱼的总重量。
若是有柯里化实现,则会是下面这样:

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
var curryWeight = function(fn) {
var _fishWeight = [];
return function() {
if (arguments.length === 0) {
return fn.apply(null, _fishWeight);
} else {
_fishWeight = _fishWeight.concat([].slice.call(arguments));
}
}
};
var fishWeight = 0;
var addWeight = curryWeight(function() {
var i=0; len = arguments.length;
for (i; i<len; i+=1) {
fishWeight += arguments[i];
}
});
addWeight(2.3);
addWeight(6.5);
addWeight(1.2);
addWeight(2.5);
addWeight(); // 这里才计算
console.log(fishWeight); // 12.5

部分内容转载自张鑫旭-鑫空间-鑫生活