共计 5039 个字符,预计需要花费 13 分钟才能阅读完成。
概念
用我本人的话来总结一下,函数柯里化的意思就是你能够一次传很多参数给 curry 函数,也能够分屡次传递,curry 函数每次都会返回一个函数去解决剩下的参数,始终到返回最初的后果。
实例
这里还是举几个例子来阐明一下:
柯里化求和函数
// 一般形式
var add1 = function(a, b, c){return a + b + c;}
// 柯里化
var add2 = function(a) {return function(b) {return function(c) {return a + b + c;}
}
}
这里每次传入参数都会返回一个新的函数,这样始终执行到最初一次返回 a +b+ c 的值。
然而这种实现还是有问题的,这里只有三个参数,如果哪天产品经理通知咱们须要改成 100 次?咱们就从新写 100 次?这很显著不合乎开闭准则,所以咱们须要对函数进行一次批改。
var add = function() {var _args = [];
return function() {if(arguments.length === 0) {return _args.reduce(function(a, b) {return a + b;})
}
[].push.apply(_args, arguments);
return arguments.callee;
}
}
var sum = add();
sum(100, 200)(300);
sum(400);
sum(); // 1000
咱们通过判断下一次是否传进来参数来决定函数是否运行,如果持续传进了参数,那咱们持续把参数都保存起来,等运行的时候全副一次性运行,这样咱们就初步实现了一个柯里化的函数。
通用柯里化函数
这里只是一个求和的函数,如果换成求乘积呢?咱们是不是又须要从新写一遍?仔细观察一下咱们的 add 函数,如果咱们将 if 外面的代码换成一个函数执行代码,是不是就能够变成一个通用函数了?
var curry = function(fn) {var _args = [];
return function() {if(arguments.length === 0) {return fn.apply(fn, _args);
}
[].push.apply(_args, arguments);
return arguments.callee;
}
}
var multi = function() {return [].reduce.call(arguments, function(a, b) {return a + b;})
}
var add = curry(multi);
add(100, 200, 300)(400);
add(1000);
add(); // 2000
在之前的办法下面,咱们进行了扩大,这样咱们就曾经实现了一个比拟通用的柯里化函数了。
兴许你想问,我不想每次都应用那个俊俏的括号结尾怎么办?
var curry = function(fn) {
var len = fn.length,
args = [];
return function() {Array.prototype.push.apply(args, arguments)
var argsLen = args.length;
if(argsLen < len) {return arguments.callee;}
return fn.apply(fn, args);
}
}
var add = function(a, b, c) {return a + b + c;}
var adder = curry(add)
adder(1)(2)(3)
这里依据函数 fn 的参数数量进行判断,直到传入的数量等于 fn 函数须要的参数数量才会返回 fn 函数的最终运行后果,和下面那种办法原理其实是一样的,然而这两种形式都太依赖参数数量了。
我在简书还看到他人的另一种递归实现办法,其实实现思路和我的差不多吧。
// 简略实现,参数只能从右到左传递
function createCurry(func, args) {
var arity = func.length;
var args = args || [];
return function() {var _args = [].slice.call(arguments);
[].push.apply(_args, args);
// 如果参数个数小于最后的 func.length,则递归调用,持续收集参数
if (_args.length < arity) {return createCurry.call(this, func, _args);
}
// 参数收集结束,则执行 func
return func.apply(this, _args);
}
}
这里是对参数个数进行了计算,如果须要有限参数怎么办?比方上面这种场景。
add(1)(2)(3)(2);
add(1, 2, 3, 4, 5);
这里次要有一个知识点,那就是函数的隐式转换,波及到 toString 和 valueOf 两个办法,如果间接对函数进行计算,那么会先把函数转换为字符串,之后再参加到计算中,利用这两个办法咱们能够对函数进行批改。参考 前端手写面试题具体解答
var num = function() {}
num.toString = num.valueOf = function() {return 10;}
var anonymousNum = (function() { // 10
return num;
}())
通过批改,咱们的函数最终版是这样的。
var curry = function(fn) {var func = function() {var _args = [].slice.call(arguments, 0);
var func1 = function() {[].push.apply(_args, arguments)
return func1;
}
func1.toString = func1.valueOf = function() {return fn.apply(fn, _args);
}
return func1;
}
return func;
}
var add = function() {return [].reduce.call(arguments, function(a, b) {return a + b;})
}
var adder = curry(add)
adder(1)(2)(3)
那么咱们说了那么多,柯里化到底有什么用呢?
预加载
在很多场景下,咱们须要的函数参数很可能有一部分一样,这个时候再反复写就比拟节约了,咱们提前加载好一部分参数,再传入剩下的参数,这里次要是利用了闭包的个性,通过闭包能够放弃着原有的作用域。
var match = curry(function(what, str) {return str.match(what);
});
match(/\s+/g, "hello world");
// [' ']
match(/\s+/g)("hello world");
// [' ']
var hasSpaces = match(/\s+/g);
// function(x) {return x.match(/\s+/g) }
hasSpaces("hello world");
// [' ']
hasSpaces("spaceless");
// null
下面例子中,应用 hasSpaces 函数来保留正则表达式规定,这样能够无效的实现参数的复用。
动态创建函数
这个其实也是一种惰性函数的思维,咱们能够提前执行判断条件,通过闭包将其保留在无效的作用域中,来看一种咱们平时写代码常见的场景。
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 的时候都会从新进行 if 语句进行判断,然而实际上浏览器的条件不可能会变动,你判断一次和判断 N 次后果都是一样的,所以这个能够将判断条件提前加载。
var addEventHandler = 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);
});
};
}
}
var addEvent = addEventHandler();
addEvent(document.body, "click", function() {}, false);
addEvent(document.getElementById("test"), "click", function() {}, false);
然而这样做还是有一种毛病,因为咱们无奈判断程序中是否应用了这个办法,然而仍然不得不在文件顶部定义一下 addEvent,这样其实节约了资源,这里有一种更好的解决办法。
var addEvent = function(el, sType, fn, capture){if (window.addEventListener) {addEvent = function(el, sType, fn, capture) {el.addEventListener(sType, function(e) {fn.call(el, e);
}, (capture));
};
} else if (window.attachEvent) {addEvent = function(el, sType, fn, capture) {el.attachEvent("on" + sType, function(e) {fn.call(el, e);
});
};
}
}
在 addEvent 函数外面对其从新赋值,这样既解决了每次运行都要判断的问题,又解决了必须在作用域顶部执行一次造成节约的问题。
React
在回家的路上我始终在想函数柯里化是不是能够扩大到更多场景,我想把函数换成 react 组件试试?我想到了高阶组件和 redux 的 connect,这两个的确是将柯里化思维用到 react 外面的体现。咱们想一想,如果把下面例子外面的函数换成组件,参数换成高阶函数呢?
var curry = function(fn) {var func = function() {var _args = [].slice.call(arguments, 0);
var func1 = function() {[].push.apply(_args, arguments)
return func1;
}
func1.toString = func1.valueOf = function() {return fn.apply(fn, _args);
}
return func1;
}
return func;
}
var hoc = function(WrappedComponent) {return function() {
var len = arguments.length;
var NewComponent = WrappedComponent;
for (var i = 0; i < len; i++) {NewComponent = arguments[i](NewComponent)
}
return NewComponent;
}
}
var MyComponent = hoc(PageList);
curry(MyComponent)(addStyle)(addLoading)
这个例子是对原来的 PageList 组件进行了扩大,给 PageList 加了款式和 loading 的性能,如果想加其余性能,能够持续在下面扩大(留神 addStyle 和 addLoading 都是高阶组件),然而写法真的很蹩脚,一点都不 coooooool,咱们能够应用 compose 办法,underscore 和 loadsh 这些库中曾经提供了。
var enhance = compose(addLoading, addStyle);
enhance(MyComponent)
总结
其实对于柯里化的使用外围还是对函数闭包的灵活运用,深刻理解闭包和作用域后就能够写出很多灵便奇妙的办法。