前面的话
高阶函数的一个高级应用就是函数柯里化。这篇文章介绍函数柯里化。
柯里化定义
函数柯里化(function currying),又称部分求值。一个currying函数首先会接受一些参数,接受这些参数之后,该函数并不会立即求值,而是继续返回一个新的函数,刚才传入的参数在函数形成闭包中保存起来。等到函数被真正需要求值的时候,之前传入的所有参数都会被一次性用于求值。
实际应用:
- 延迟计算
- 动态创建
- 参数复用
延迟计算
一个计算每月开销函数:
var mouthCost = 0;
var cost = function (money) {
mouthCost += money;
}
cost(100);// 第一天开销
cost(200);
cost(300);
// ...
cost(300);// 第30天开销
alert(mouthCost);// 总额
我们发现其实我们只在每月的最后一天才求值计算,其他的我们只需要保存每天的数据就行了,改写上面的代码
var cost = (function () {
var args = [];
return function () {
// 如果没有参数,就计算args数组中的和
if(arguments.length === 0) {
var money = 0;
for(let i = 0,len = args.length; i< len; i++) {
money += args[i];
}
return money;
}else {
[].push.apply(args, arguments);
}
}
})();
cost(100);
cost(200);
cost(300);
console.log(cost());// 600
上面的例子使用了闭包与立即执行函数来实现,还不是真正意义上的currying函数。
编写一个通用的柯里化函数,它接受一个要被柯里化的函数和一些必要的参数
var currying = function (fn) {
var args = [];
return function () {
if(arguments.length === 0) {
return fn.apply(null, args); // 当参数长度为0时,计算求值
} else {
[].push.apply(args, arguments);
return arguments.callee; // 返回拥有arguments对象的函数
}
}
};
var cost = (function() {
var money = 0;
return function () {
for(let i = 0,len = arguments.length;i< len ;i++) {
money += arguments[i];
}
return money;
}
})();
var cost1 = currying(cost);
console.log(cost1(100));// 缓存参数
console.log(cost1(200));// 缓存参数
console.log(cost1());// 计算
再看一个部分求和的例子:
function curry (fn) {
// 返回的args数组是从实参的第二个开始所有的参数 这里是 5
var args = Array.prototype.slice.call(arguments, 1);
return function () {
// 返回的args数组是从所有的实参 这里是 3
var innerArgs = Array.prototype.slice.call(arguments);
// 拼接数组
var finalArgs = args.concat(innerArgs);
// 执行add()函数,参数是finalArgs
return fn.apply(null, finalArgs);
};
}
function add(num1, num2){
return num1 + num2;
}
var curriedAdd = curry(add, 5);
console.log(curriedAdd(3));// 8
curry 函数的第一个参数fn是要进行柯里化的函数,其他参数是要传入的值。
也可以直接这样写:
var curriedAdd = curry(add, 5, 12);
console.log(curriedAdd());// 17
这样args就是[5, 12], innerArgs就是[]空数组。
动态创建函数
应用场景:
每一次调用函数都需要进行一次判断,但其实第一次判断计算之后,后续调用并不需要再次判断,这种情况非常适合使用柯里化函数来处理。即第一次判断之后,动态创建一个新函数用于处理后续传入的参数,并返回这个新参数。
例如:在DOM中添加事件时要兼容现代浏览器和IE浏览器(IE< 9),方法就是对浏览器环境进行判断,看浏览器是否支持
function addEvent(type, el, fn, capture = false) {
if(window.addEventListener) {
el.addEventListener(type, fn, capture);
}
else if(window.attachEvent){
el.attachEvent('on'+ type,fn);
}
}
这样的写法,就是每一次添加事件都会调用做一次处理,那么有没有办法只判断一次?可以利用闭包与立即执行函数来处理。
const addEvent = (function () {
if(window.addEventListener) {
return function (type, el, fn, capture) {
el.addEventListener(type, fn, capture);
}
}
else if(window.attachEvent) {
return function (type, el, fn) {
el.attachEvent('on'+ type,fn);
}
}
}());
[bind()函数]
bind函数与call、apply函数有所区别:
let obj = {
name: 'xiaoqi';
}
const fun = function () {
console.log(this.name);
}.bind(obj)
fun();// xiaoqi
bind()用来改变执行时候的上下文,但是函数本身并不执行,本质上是延迟计算,与call/apply执行有所不同。
参数复用
上一篇高阶函数中,用到了isType函数判断对象的类型,其原理是Object.prototype.toString.call(obj).这次试用bind将来实现。
const toStr = Function.prototype.call.bind(Object.prototype.toString);
console.log(toStr([1, 2, 3]));
console.log(toStr('123'));
console.log(toStr(123));
首先试用Function.prototype.call函数指定一个this值,然后,.bind返回一个新的函数,始终将Object.prototype.toString设置为传入参数,其实等价为Object.prototype.toString.call()
实现currying函数
最后实现一个复杂的currying函数
现在我们知道所谓柯里化函数,就是用闭包把传入的参数保存起来,当传入参数的数量足够函数时,就开始执行函数。
{
function currying(fn, length) {
length = length || fn.length;
return function() {
// 实参长度大于fn.length
return arguments.length >= length?
// 满足时,执行fn函数
fn.apply(null, arguments):
//不满足时,递归currying函数,新的fn为bind返回的新函数(绑定了...arguments,但不执行,)
// 新的length为原fn剩余的参数长度
currying(fn.bind(null, ...arguments), length - arguments.length);
}
}
const fn = currying(function(a, b, c){
console.log([a, b, c]);
});
fn('a', 'b', 'c');
fn('a', 'b')('c');
fn('a')('b')('c');
fn('a')('b','c');
}