就是操作函数的函数,接受一个或多个函数作为参数,并返回一个新函数。
数学中,函数 f
的概念就是对于输入x,产生一个输出y=f(x),就是对于相同的输入,永远会得到相同的输出,而且没有任何可观察的副作用,也不依赖外部环境的状态。
var arr = [1,2,3,4,5];
// Array.prototype.slice 是纯函数,没有副作用,对于固定的输入,输出也总是固定
arr.slice(0,3); //[1,2,3]
arr.slice(0,3); //[1,2,3]
// Array.prototype.splice 不纯,有副作用,对于固定输入,输出不固定
arr.splice(0,3); //[1,2,3]
arr.splice(0,3); //[4,5]
函数式编程中,我们想要的就是 slice
这样的纯函数,而不是 splice
这样的每次调用后会把数据弄乱的函数。
// 不纯的 checkage 函数的行为不仅取决于输入参数age,还取决于外部变量 min
// 即函数的行为需要由外部系统环境决定
// 一旦需求复杂,则造成了系统的复杂性
var min = 18;
var checkage = age => age > min;
// 纯的,很函数式
var checkage = age => age > 18;
可以看到,纯的 checkage
把关键数字 18 硬编码在函数内部,扩展性差,所以需要用到函数的柯里化
柯里化 定义:是把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数而且返回结果的新函数的技术。
比如 var add = (x,y) => x+y;
可以这样:
var add = function(x){
return function(y){
return x + y;
}
}
//es6
var add = x => (y => x + y);
var add2 = add(2);
add2(2); //4
var add100 = add(100);
add100(2); //102
这样简单的函数使用柯里化没什么意义,比如之前的 checkage
:
var checkage = function(min){
return function(age){
return age > min;
}
}
var checkage18 = checkage(18);
checkage18(20); //true
// 判断数据类型
function isType(type){
return function(obj){
return Object.prototype.toString.call(obj) === "[object " + type + "]"
}
}
var isString = isType('String')
var isNumber = isType('Number')
var isBoolean = isType('Boolean')
为什么要柯里化呢?
函数的柯里化能够让你重新组合你的应用,把你的复杂功能拆分成一个个小部分,每一个小部分都是简单的,便于理解的,容易测试的。
如何柯里化?
function print(name){
return function(age){
return name + 'age is:' + age
}
}
print
函数虽然进行了柯里化,但是我们肯定不想每次在需要柯里化的时候,都像上面那样进行函数的嵌套,那是噩梦!所以我们需要一个帮助其它函数进行柯里化的函数:
function curry(fn){
var args = Array.prototype.slice.call(arguments,1); // 除 fn 外的参数
return function(){
var newArgs = Array.prototype.slice.call(arguments); // 新函数的全部参数
var totalArgs = args.concat(newArgs); // 合并的参数
return fn.apply(this,totalArgs);
}
}
下面使用 curry
来柯里化 showMessage
:
function showMessage(name,age,height){
console.log(name)
console.log(age)
console.log(height)
}
var sm = curry(showMessage,'yy')
sm(25,190)
- args -> ['yy']
- newArgs -> [25,190]
- totalArgs -> ['yy',25,190]
柯里化使用场景
-
可以使用一些小技巧
-
提前绑定好函数里面的某些参数,达到参数复用的效果,提高了适用性.
// 兼容事件绑定
var addEvent = function (el, type, fn, capture) {
if (window.addEventListener) {
el.addEventListener(type, fn, capture);
}
else {
el.attachEvent('on' + type, fn);
}
};
//但是上面每次都会运行 if ,产生不必要开销,我们可以这样:
var addEvent = (function () {
if (window.addEventListener) {
return function (el, type, fn, capture) {
el.addEventListener(type, fn, capture);
}
}
else {
return function (el, type, fn) {
var IEtype = 'on' + type;
el.attachEvent(IEtype, fn);
}
}
})();
- 固定易变因素
Function.prototype.bind
- 延迟计算
function add() {
var args = Array.prototype.slice.call(arguments);
var _that = this;
return function() {
var newArgs = Array.prototype.slice.call(arguments);
var total = args.concat(newArgs);
if(!arguments.length) {
var result = 1;
for(var i = 0; i < total.length; i++) {
result *= total[i];
}
return result;
}
else {
return add.apply(_that, total);
}
}
}
add(1)(2)(3)(); // 6
add(1, 2, 3)(); // 6
学会使用纯函数和柯里化后,容易写出『包菜式』代码:
虽然这也是函数式,但是不优雅,我们需要用到函数组合:
var compose = function(f,g){
return function(x){
f(g(x))
}
}
var add1 = x => x + 1
var mul5 = x => x * 5
compose(mul5,add1)(2) //15
Point Free
中文意思是,不要命名转瞬即逝的中间变量
// str 作为中间变量,除了让代码变长了一点外,没有别的意义
var f = function(str){
str.toUpperCase().split(' ')
}
// 改造一下
var toUpperCase = function(word){
return word.toUpperCase()
}
var split = function(x){
return function(str){
return str.split(x)
}
}
var f = compose(split(' '),toUpperCase);
f('abcd efgh')
这种风格能够帮助我们减少不必要的命名,让代码保持简洁和通用。
注:这是早些年的一些总结,现在来看已经没什么意义了。