前言
最初接触到函数式编程,是因为一道面试题,当时年少轻狂,当时就问面试官:你是不是搞错了??人家说:没有!现在想想当时的场景也是隐隐感觉到尴尬,那道面试题很简单,如下:
> 如何实现以下执行结果: add(1)(2) = 3;
因为当时也没见过这种写法,所以当时是“懵逼树上懵逼果,懵逼树下你和我...”,下来赶紧查资料,然后就首次接触到了“函数式编程”这个名词,也是从那以后开始关注函数式编程,时至今日,我早已退去了当年的稚嫩,打算谈一谈关于javascript中的函数式编程。
高阶函数
在之前收集资料的时候,其实最先开始认识到的是高阶函数这个词,所以在了解函数式编程之前呢,我们要先来看一下什么是高阶函数
定义
- 可以将其他函数作为参数传递
- 可以将函数作为返回值
以上两个特性,具备任何一个都可以称之为高阶函数。根据这两个特性,我们仔细想下,会发现我们在使用js编写程序的时候,有好多符合以上两条或至少一条特性的方法,比如:
1、map、reduce、filter、sort(函数作为参数)
2、闭包(返回一个函数)
3、$.ajax(url, function(res){console.log(res)})(回调函数作为参数)
由此可以见,平常代码中我们已经非常频繁的在使用高阶函数了,只是我们一直都不知道,他们还有一个名字,叫做高阶函数。
高阶函数我们现在有个基本的概念了,那么我们今天的主角,函数式编程,到底是个什么东西呢?
函数式编程
起源
说起函数式编程,就不得不提到一个数学方面的知识“范畴论”,这个词感觉好牛*啊,什么是范畴呢?
1、范畴就是使用箭头连接的物体
2、彼此存在某种关系的事物,都可以构成范畴
3、箭头称为“态射”,成员可以通过某种态射转换成另一个成员
为了更生动描述范畴,这里盗用阮一峰大大的一张图
也许对于我们写代码的民工来说,这里接起来可能还是优点抽象,能不能用代码说明呢?当然可以:
以hello word为例,我们可以使用一些方法将它进行转换
// 将hello word转为大写
let upperCase = function(x) {
return x.toUpperCase();
}
// 将hello word后面加上!
let addStr = function(x) {
return `${x}!`;
}
如上图,我们可以将一个对象,通过某一些方法将这个对象的状态有一个状态改变成为另外一种状态,我们就说他们属于一个范畴里的事物,此时代码中我们也是可以和范畴论里的概念对得上的,即:
函数=>态射
值=>成员
容器=>范畴
函数式编程的定义
- 首先,它不是一个库,它是一种编程范式,与面向对象编程和面向过程编程并列
- 强调将计算过程分解成可复用的函数,就像模块化编程里的components
- 强调函数是一等公民
- 只有纯函数才是合格的函数
《函数式编程》中有一句总结非常好
函数式编程就是通过使用函数来将值转换成抽象单元,接着用于构建软件系统
函数式编程的一些特性
1、函数是一等公民
顾名思义,一等公民就是和指其他对象一样,无犯罪记录及其他特权的普通公民,他即可以作为参数被传递又可以作为值输出
2、使用纯函数
1)首先纯函数是一种函数
2)对于相同的输入,永远会得到相同的输出,如下代码:
let arr = [0, 1, 2, 3, 4, 5, 6];
// 纯的
arr.slice(0, 3); //=> [0, 1, 2]
arr.slice(0, 3); //=> [0, 1, 2]
// 不纯的
arr.splice(0, 3); //=> [0, 1, 2]
arr.splice(0, 3); //=> [3, 4, 5]
3)执行函数不产生任何可观察的副作用,如下代码:
// 不纯的
let minnum = 21;
let checkAge = function(age) {
return age > minnum; // 依赖外部变量
}
// 纯的
let checkAge = function(age) {
let minnum = 21;
return age > minnum; // 不依赖外部变量
}
我们在平时的开发中,也常常因为依赖外部变量而导致一些意想不到的Bug,正如《js函数式编程指南》中所提到的纯函数:
就像一潭死水中的“水”本身并不是幼虫的培养器,“死”才是生成虫群的原因。同理,副作用中的“作用”本身并没有什么坏处,“副”才是滋生 bug 的温床。
3、函数柯里化
又遇到了一个奇怪的名字,我们来看下柯里化是的定义:函数柯里化,即只传递给函数一部分参数来调用它,让它返回一个函数去处理剩下的参数。
嗯?这不就是说的我之前遇到的那个面试题目吗?结合我们前面所提到的高阶函数,我们来看下如何实现:
function add(x) {
return function(y) {
return x + y;
}
}
let result = add(1)(2);
console.log(result) // 3
我们先定义一个函数,它接收一个参数x,然后我们在它内部重新返回来一个函数,这个函数同样接收一个参数y,在这个函数内部将x + y的结果返回回来,最后得到add(1)(2) = 3的结果。
现在我们实现了一个最简单的柯里化函数,接下来我们再看一个更高级的问题:如何实现add(1)(2)(3)....的结果呢?通过对函数柯里化定义的理解,我们可以升级下上面的代码:
function aadd() {
let _args = [].slice.call(arguments);
let _adder = function() {
[].push.apply(_args, [].slice.call(arguments));
return _adder;
};
_adder.toString = function() {
return _args.reduce(function (a, b) {
return a + b;
});
}
return _adder;
}
let result1 = add(1)(2)(3); // 6
console.log(result1(2)(1) + 1); // 10
我们现在实现了多参数柯里化,不过还有一个事情,如果我现在已经有了一个函数,然后想把它转成函数柯里化的形式,要怎么做呢,比如将下面一个函数转成柯里化
let add = function(a, b, c) {
return a + b + c;
}
console.log(add(1, 2, 3)); // 6
现在将上面的函数柯里化
function curry(fn, args) {
let length = fn.length; // 函数参数的长度
_args = args || [];
return function() {
[].push.apply(_args, [].slice.call(arguments));
if (_args.length < length) {
// 自己调用自己,将保存的参数传递到下一个柯里化函数。
return curry.call(this, fn, _args);
} else {
// 如果传入的参数列表长度已经超过函数定义时的参数长度,就执行。
return fn.apply(this, _args);
}
}
}
// 柯里化
let add = curry(function(a, b, c) {
return a + b + c;
})
console.log(add(1)(2)(3));
总结
说了那么多,函数式编程有什么好处呢?
- 参数复用
- 延迟执行
- 减少代码冗余度,提升代码可读性