带你入门函数式编程
一、什么是函数式编程
1.概念
首先,函数式编程是一种编程范型。其他的编程范型有面向过程编程(关注计算机的执行步骤,指定先做这个再做那个),还有面向对象(创建对象然后对象又有方法然后又可以改变它们)等等。函数式编程也是编程范型,函数为王。**
这里需要介绍下编程范型的概念,编程范型/编程范式(范即模范、典范之意,范型即模式、方法)是一类典型的编程风格,是指从事软件工程的一类典型的风格。如函数式编程、过程式编程、面向对象编程、指令式编程等等为不同的编程范型。
编程范型提供了(同时决定了)程序员对程序执行的看法。例如,在面向对象编程中,程序员认为程序是一系列相互作用的对象,而在函数式编程中一个程序会被看作是一个无状态的函数计算的序列。
函数式编程也是一种编程风格,完成项目时怎样组织编写代码。
函数式编程更是一种观念,可以以一种思考问题的方式来完成任务,也是一种趋势。
2.优点
- 更安全,更容易调试,构建项目时更好去维护:函数式编程不依赖、也不会改变外界的状态,只要给定输入参数,返回的结果必定相同。因此,每一个函数都可以被看做独立单元,很有利于进行单元测试和除错,以及模块化组合。
- 有庞大的使用函数式编程开发JS的开发者社区
- 代码简洁,抽象程度高,容易复用,开发快速:函数式编程大量使用函数,减少了代码的重复,因此程序比较短,开发速度较快,抽象程度高,也更容易复用。
- 接近自然语言,易于理解:函数式编程的自由度很高,可以写出很接近自然语言的代码。如以下代码:(merge([1,2],[3,4]).sort().search("2"),应该一眼就能明白它的意思。
- 易于"并发编程":函数式编程不需要考虑"死锁",因为它不修改变量,所以根本不存在"锁"线程的问题。不必担心一个线程的数据,被另一个线程修改,所以可以很放心地把工作分摊到多个线程,部署"并发编程"。
3.使用指南
(1)在函数式编程中,一切都要函数化
要用函数实现程序,给定输入,会有一个输出,我们更多的要考虑数据的输入输出流,需要想怎样用函数表达一切。
- 非函数化:(命令式风格)
const name = "ming";
const greeting = "hello,";
console.log(greeting + name); // hello,ming
- 函数化:
function greet(name) {
return `hello,${name}`;
}
greet("ming"); // hello,ming
(2)使用纯函数,避免副作用
副作用:副作用是指在计算过程中,系统状态的一种变化,或者是与外部进行的可观察的交互。
副作用可能包含,但不限于:
- 更改文件系统
- 往数据库插入记录
- 发送一个 http 请求
- 可变数据
- 打印/log
- 获取用户输入
- DOM 查询
- 访问系统状态
函数式编程的哲学就是假定副作用是造成不正当行为的主要原因。
当然这并不是说,要禁止使用一切副作用,而是说,要让它们在可控的范围内发生。
纯函数:针对相同的一组输入,会永远得到相同的输出,而且没有任何可观察的副作用,这样的函数叫纯函数。
用一个例子来说明下。数组里的slice和splice方法,这两个函数的作用是一样的,但是需要注意的是,splice会改变原数组,而slice不会改变原数组。所以slice每次用相同的输入去执行都会得到相同的输出,符合纯函数的定义。而splice改变了原数组,每次用相同的输入去执行都会得到不同的输出,产生了副作用。
let arr = [1,2,3,4,5];
// 纯的
arr.slice(0,3); // [1,2,3]
arr.slice(0,3); // [1,2,3]
arr.slice(0,3); // [1,2,3]
// 不纯的
arr.splice(0,3); // [1,2,3]
arr.splice(0,3); // [4,5]
arr.splice(0,3); // []
再看另外一个例子:
// 不纯的
var b = 21;
var test = function(a) {
return a >= b;
};
// 纯的
var test = function(a) {
var b = 21;
return a >= b;
};
在不纯的版本中,函数外部的变量会影响到函数的返回结果,或者说它引入了外部的环境。
而在纯的版本中,函数就可以自己保持“独立”。
(3)使用高阶函数:函数可以作为输入/输出
function makeAdjectifier(adjective) {
return function (string) {
adjective + " " + string;
};
}
var coolifier = makeAdjectifier("cool");
coolifer("conference"); // => "cool conference"
不要使用for/while等迭代,使用map,reduce,filter这样的高阶函数,可以作为一个函数去应用。
(4)避免可变性:使用不可变数据
不好的做法:
let arr = ['1','2','3'];
arr[2] = '4';
console.log(arr); // ['1,'2','4']
好的做法:
const arr = ['1','2','3'];
const newArr = arr.map(item => {
if(item === '3') {
return '4';
}
return item;
});
console.log(arr); // ['1,'2','3']
console.log(newArr); // ['1,'2','4']
(5)持久的数据结构,实现高效的不变性
当我们将事物视为不变时,我们最终所做的事情就是为所有事物制作新的副本。
如果有一个数组[1,2,3],我们要改变3为4,那么在可变的环境中直接把3换为4即可,但是之前说过要避免这种操作,就要取而代之,复制一个新数组,复制1,2然后输入4,就需要花更多时间存储两个数组,这是非常可怕的。我们可以把这个数组当做一棵树,树上的叶子结点就是要存储东西的地方,如果想改变某个数据,只需要制造一个新节点存4,可以新建一个有1,2,4的树,可以服用之前已经存在的数据。这个叫做数据共享。这样我们可以不用浪费很多时间和存储空间去更新数据。
二、柯里化
概念:只传递给函数一部分参数来调用它,让它返回一个函数去处理剩下的参数。
var add = function(x) {
return function(y) {
return x + y;
};
};
var increment = add(1);
var addTen = add(10);
increment(2);
// 3
addTen(2);
// 12
上面我们定义了一个add函数,它接收一个参数,并返回一个新的函数。调用add后,返回的函数就以闭包的方式记住了它的第一个参数x。
三、组合
组合其实就是你可以选择两个函数,让它们结合,产生一个新的函数。这样写可以使我们的代码更加优雅。
var compose = function(f,g) {
return function(x) {
return f(g(x));
};
};
f 和 g 都是函数,x 是在它们之间通过“管道”传输的值。
var toUpperCase = function(x) { return x.toUpperCase(); };
var exclaim = function(x) { return x + '!'; };
var shout = compose(exclaim, toUpperCase);
shout("send in the clowns");
//=> "SEND IN THE CLOWNS!"
参考:
Learning Functional Programming with JavaScript, by Anjana Vakil — JSUnconf 2016
函数式编程初探
编程范型wiki
函数式编程指北中文版
更多文章以及分享请关注微信公众号 前端er的分享,不止于前端,定期输出一些技术知识、生活感想、理财知识等。