函数式编程入门
一、 面向过程编程、面向对象编程、函数式编程概要
1.命令式编程:即过程式编程。强调怎么做。
2.面向对象编程: 通过对一类事物的抽象,即class,其中对象是基本单元。常用的是继承方式。 平时会看到生命周期、链式调用。比如react中的类组件。
3.函数式编程:即声明式编程。强调做什么。更加符合自然语言。常用的是组合的方式。平时看到的数据驱动(响应式编程)。比如react的函数组件+hooks。
二、函数式编程特性
1.纯函数:相同的输入,永远会得到相同的输出。即也就是数学函数。
具体理解两点: 没有副作用(数据不可变): 不修改全局变量,不修改入参。最常见的副作用就是随意操纵外部变量,由于js对象是引用类型。不依赖外部状态(无状态): 函数的的运行结果不依赖全局变量,this 指针,IO 操作等。如下:
// 非纯函数
const curUser = {
name: 'Peter'
}
const saySth = str => curUser.name + ': ' + str; // 引用了全局变量
const changeName = (obj, name) => obj.name = name; // 修改了输入参数
changeName(curUser, 'Jay'); // { name: 'Jay' }
saySth('hello!'); // Jay: hello!
// 纯函数
const curUser = {
name: 'Peter'
}
const saySth = (user, str) => user.name + ': ' + str; // 不依赖外部变量
const changeName = (user, name) => ({...user, name }); // 未修改外部变量
const newUser = changeName(curUser, 'Jay'); // { name: 'Jay' }
saySth(curUser, 'hello!'); // Peter: hello!
|
2.通过事例进一步加深对纯函数的理解
let arr = [1,2,3];
arr.slice(0,3); //是纯函数
arr.splice(0,3); //不是纯函数,对外有影响
function add(x,y){ // 是纯函数
return x + y // 无状态,无副作用,无关时序,幂等
} // 输入参数确定,输出结果是唯一确定
let count = 0; //不是纯函数
function addCount(){ //输出不确定
count++ // 有副作用
}
function random(min,max){ // 不是纯函数
return Math.floor(Math.radom() * ( max - min)) + min // 输出不确定
} // 但注意它没有副作用
function setColor(el,color){ //不是纯函数
el.style.color = color ; //直接操作了DOM,对外有副作用
}
|
3.强调使用纯函数的意义是什么,也就是说函数式编程的特性是什么。
更少的 Bug:使用纯函数意味着你的函数中不存在指向不明的 this,不存在对全局变量的引用,不存在对参数的修改,这些共享状态往往是绝大多数 bug 的源头。
可缓存性:因为相同的输入总是可以返回相同的输出,因此,我们可以提前缓存函数的执行结果。
自文档化:由于纯函数没有副作用,所以其依赖很明确,因此更易于观察和理解。配合类型签名(一种注释)更清晰。
便于测试和优化: 相同的输入永远会返回相同的结果,因此我们可以轻松断言函数的执行结果,同时也可以保证函数的优化不会影响其他代码的执行。
4.在实际的react开发中,也会看到纯函数的应用。


三、函数式编程理解
1.数学函数和范畴学
数学函数:在数学上,学习一次函数、二次函数等一个特点就是输入x通过函数都会返回有且只有一个输出值y。这里的函数充当映射关系的桥梁。这也正是函数式编程要求必须是纯的原因
范畴:包括值 和 值的变形关系(函数)。即范畴好似一个容器包含这两样东西。
2.js中的函数
JavaScript 语言将函数看作一种值,与其它值(数值、字符串、布尔值等等)地位相同。凡是可以使用值的地方,就能使用函数。比如,可以把函数赋值给变量和对象的属性,也可以当作参数传入其他函数,或者作为函数的结果返回。函数只是一个可以执行的值,此外并无特殊之处。由于函数与其他数据类型地位平等,所以在 JavaScript 语言中又称函数为第一等公民。
包括 普通函数 和 剪头函数 。剪头函数(简洁同时this指向定义的时候已经确定)也是函数式编程中的一个使用体现。
3.命令式与声明式
前面提到说函数式编程更加符合自然语言,就是因为其用了声明式。举个例子对比一下
// 命令式-----强调怎么做
var makes = [];
for (i = 0; i < cars.length; i++) {
makes.push(cars[i].make);
}
// 声明式-----做什么
var makes = cars.map(function(car){ return car.make; });
|
4.高阶函数
高阶函数:参数或返回值为函数的函数。比如可以用于拦截和监控,比如防抖和节流的实际开发中的应用(下面的例子是不考虑this的情况)
防抖的例子:在time的时间内,你连续点击几次,会先清除计时器clearTimeout,当你停下来的时候,根据最后一次点击等待time时间执行func
function debounce(func, time) {
let timeout = null;
return function() {
if (timeout) {
clearTimeout(timeout)
}
timeout = setTimeout(() => {
timeout = null;
func.apply(null, arguments) // 假如不考虑this
}, time);
}
}
|
节流的例子:if语句的执行,取决与setTimeout中对flag值的恢复。time时间到了,就执行一次。这个里面setTimeout()和 func(),一个在前,一个在后,但是执行顺序并不是同步执行。这里的setTimeout是异步的。前面的《理解JS异步》有介绍。
function throtle(func, time) {
let flag = true;
return function () {
if (flag) {
flag = false;
setTimeout(() => {
flag = true
}, time);
func.apply(null, arguments) // 假如不考虑this
}
}
|
所以呢,高阶函数的这两个例子也就是声明式的,可以当做工具函数,我们用到的时候调用这两个函数就行。因此这样的函数的名字起的语义化就由为重要。
5.纯函数
再次提起纯函数:没有副作用(不修改外部变量)+无状态(不依赖外部变量)。具体的跳会前面的介绍查看。
当了解完函数式编程的基本概念和要点后,然而让我们利用函数式编程可能还无法下手。这时候我们用上几个好的方法规范自己编程更加高效。所以下面的6-11可以说成都是编写函数式程序的方法or工具。
6.函数柯里化(curry)
a. 什么是柯里化
柯里化指的是将一个多参数的函数拆分成一系列单参数函数。
// 柯里化之前
function add(x, y) {
return x + y;
}
add(1, 2) // 3
// 柯里化之后----ES5写法
function add(x) {
return function (y) {
return x + y;
};
}
add(1)(2) // 3
// 柯里化之后----ES6的写法
const add = x => y => x + y;
|
b. 柯里化的应用
eg1:获取数组对象的某个属性用柯里化优化
比如我们有这样一段数据:
let person = [{name: 'kevin'}, {name: 'daisy'}];
如果我们要获取所有的 name 值,我们可以这样做
let name = person.map(function (item) {
return item.name;
})
不过如果我们有 curry 函数:
let prop = curry(function (key, obj) {
return obj[key]
});
let name = person.map(prop('name'))
我们为了获取 name 属性还要再编写一个 prop 函数,是不是又麻烦了些?
但是要注意,prop 函数编写一次后,以后可以多次使用,实际上代码从原本的三行精简成了一行,而且你看代码是不是更加易懂了。
|
eg2:高级柯里化,可以看看 ramda库中提供的curry,它提供的好像和我们上面提到的概念不一致,原因参数不需要一次只传入一个 & 占位符值R.__ _表示R.__,表示还未传入的参数 。所以可以称作为高级柯里化
const addFourNumbers = (a, b, c, d) => a + b + c + d; const g = R.curry(addFourNumbers); // 每次都单参数也可以 g(1)(2)(3) // 多参数也可以 g(1)(2, 3) g(1, 2)(3) g(1, 2, 3) g(_, 2, 3)(1) g(_, _, 3)(1)(2) g(_, _, 3)(1, 2) g(_, 2)(1)(3) g(_, 2)(1, 3) g(_, 2)(_, 3)(1) |
c. 柯里化的实现
function curry(func) {
return function curried(...args) {
if (args.length >= func.length) { // 通过函数的length属性,来获取函数的形参个数
return func.apply(this, args);
} else {
return function (...args2) {
return curried.apply(this, args.concat(args2));
};
}
}
}
|
7.偏函数
a.偏函数是什么
偏函数:固定任意元参数,在平时开发中用到的如下:
// 假设一个通用的请求 API
const request = (type, url, options) => ...
// GET 请求
request('GET', 'http://a....')
request('GET', 'http://b....')
request('GET', 'http://c....')
// POST 请求
request('POST', 'http://....')
// 但是通过部分调用后,我们可以抽出特定 type 的 request
const get = request('GET');
get('http://', {..})
|
b.柯里化与偏函数的区别:
- 柯里化是将一个多参数函数转换成多个单参数函数,也就是将一个 n 元函数转换成 n 个一元函数。
- 偏函数则是固定一个函数的一个或者多个参数,也就是将一个 n 元函数转换成一个 n - x 元函数。
c.偏函数的实现
function partial(fn) {
let args = [].slice.call(arguments, 1);
return function () {
const newArgs = args.concat([].slice.call(arguments));
return fn.apply(this, newArgs);
};
}
|
8.惰性函数
a. 惰性函数是什么
惰性函数解决每次都要进行判断的这个问题,解决办法是重写函数.
eg:我们现在需要写一个 foo 函数,这个函数返回首次调用时的 Date 对象,注意是首次。
// 普通方法 : 一是污染了全局变量,二是每次调用 foo 的时候都需要进行一次判断
let t;
function foo() {
if (t) return t;
t = new Date()
return t;
}
// 闭包 : 没有解决调用时都必须进行一次判断的问题
let foo = (function() {
let t;
return function() {
if (t) return t;
t = new Date();
return t;
}
})();
// 函数对象: 依旧没有解决调用时都必须进行一次判断的问题
function foo() {
if (foo.t) return foo.t;
foo.t = new Date();
return foo.t;
}
// 惰性函数 : 以上两个存在问题都解决了 (只需要判断一次)
let foo = function() {
let t = new Date();
// 重写 foo函数
foo = function() {
return t;
};
return foo();
};
|
b.惰性函数的应用
eg:DOM 事件添加中,为了兼容现代浏览器和 IE 浏览器,我们需要对浏览器环境进行一次判断。
// 普通写法----问题在于我们每当使用一次 addEvent 时都会进行一次判断
function addEvent (type, el, fn) {
if (window.addEventListener) {
el.addEventListener(type, fn, false);
}
else if(window.attachEvent){
el.attachEvent('on' + type, fn);
}
}
// 惰性函数写法----判断一次
function addEvent (type, el, fn) {
if (window.addEventListener) {
addEvent = function (type, el, fn) {
el.addEventListener(type, fn, false);
}
}
else if(window.attachEvent){
addEvent = function (type, el, fn) {
el.attachEvent('on' + type, fn);
}
}
addEvent(type, el, fn);
}
// 或者使用闭包-----判断一次
let addEvent = (function(){
if (window.addEventListener) {
return function (type, el, fn) {
el.addEventListener(type, fn, false);
}
}
else if(window.attachEvent){
return function (type, el, fn) {
el.attachEvent('on' + type, fn);
}
}
})();
|
9.函数组合
a.什么是函数组合
函数组合将多个函数合成一个函数,同时也遵循数学上的结合律。
const compose = (f, g) => x => f(g(x)); const f = x => x + 1; const g = x => x * 2; const fg = compose(f, g); fg(1) //3 // 函数组合满足结合律 compose(f, compose(g, t)) = compose(compose(f, g), t) = f(g(t(x))); |
b. 函数组合的应用
eg:我们需要写一个函数,输入 'kevin',返回 'HELLO, KEVIN'
// 使用组合前
let toUpperCase = function(x) { return x.toUpperCase(); };
let hello = function(x) { return 'HELLO, ' + x; };
let greet = function(x){
return hello(toUpperCase(x));
};
greet('kevin');
// 使用组合后----代码从右向左运行
let compose = function(f,g) {
return function(x) {
return f(g(x));
};
};
let greet = compose(hello, toUpperCase);
greet('kevin');
|
eg: 比如将数组最后一个元素大写,假设 log, head,reverse,toUpperCase 函数存在
const upperLastItem = compose(log, toUpperCase, head, reverse); // 也可以如下组合: // 组合方式 1 const last = compose(head, reverse); const shout = compose(log, toUpperCase); const shoutLast = compose(shout, last); // 组合方式 2 const lastUppder = compose(toUpperCase, head, reverse); const logLastUpper = compose(log, lastUppder); |
c. 函数组合的优点
通过上面的例子看出,不同的组合方式,我们可以轻易组合出其他常用函数,让我们的代码更具表现力。
d. 函数组合的实现
// 从右向左结合 const compose = (...fns) => (...args) => fns.reduceRight((val, fn) => fn.apply(null, [].concat(val)), args); |
e.pointfree是什么
pointfree 指的是函数无须提及将要操作的数据是什么样的。pointfree 模式能够帮助我们减少不必要的命名,让代码保持简洁和通用
// 非 pointfree,因为提到了数据:name
let initials = function (name) {
return name.split(' ').map(compose(toUpperCase, head)).join('. ');
};
// pointfree
let initials = compose(join('. '), map(compose(toUpperCase, head)), split(' '));
initials("hunter stockton thompson");
|
10.函数记忆
a. 函数记忆是什么
函数记忆是指将上次的计算结果缓存起来,当下次调用时,如果遇到相同的参数,就直接返回缓存中的数据。
function add(a, b) {
return a + b;
}
// 假设 memorize 可以实现函数记忆
let memorizedAdd = memoize(add);
memorizedAdd(1, 2) // 3
memorizedAdd(1, 2) // 相同的参数,第二次调用时,从缓存中取出数据,而非重新计算一次,这样可以优化性能
|
b. 函数记忆的应用
eg: 以斐波那契数列为例(如果需要大量重复的计算,或者大量计算又依赖于之前的结果,便可以考虑使用函数记忆)
// 未使用前
let count = 0;
let fibonacci = function(n){
count++;
return n < 2? n : fibonacci(n-1) + fibonacci(n-2);
};
for (let i = 0; i <= 10; i++){
fibonacci(i)
}
console.log(count) // 453
// 使用函数记忆后
let count = 0;
let fibonacci = function(n) {
count++;
return n < 2 ? n : fibonacci(n - 1) + fibonacci(n - 2);
};
fibonacci = memorize(fibonacci);
for (let i = 0; i <= 10; i++) {
fibonacci(i)
}
console.log(count) // 12
|
c. 函数记忆的实现
function memorize(fn) {
const cache = Object.create(null); // 存储缓存数据的对象
return function (...args) {
const _args = JSON.stringify(args);
return cache[_args] || (cache[_args] = fn.apply(fn, args));
};
};
|
11.函子
Functor函子;Maybe函子;Monad函子;基础概念的理解参考此博文。
最后,更多学习可以结合Ramda库。

浙公网安备 33010602011771号