面向对象编程(OOP)通过封装变化使得代码更易理解。 函数式编程(FP)通过最小化变化使得代码更易理解。
-- Michacel Feathers(Twitter)
什么是函数式编程
简单的说,函数式编程倡导利用若干简单的执行单元让计算结果不断渐进,逐层推导复杂的运算。函数式编程有两个最基本的运算:合成(Compose)和柯里化(Currying)。
合成(Compose)
如果一个值要经过多个函数,才能变成另外一个值,就可以把所有中间步骤合并成一个函数,这叫做函数的合成(compose)。合成的好处显而易见,它让代码变得简单而富有可读性,同时通过不同的组合方式,我们可以轻易组合出其他常用函数,让我们的代码更具表现力。
我们在工作中经常会遇到需要按顺序执行一些函数的情况,如下所示:
function f1(arg) {
console.log("f1", arg);
return arg;
}
function f2(arg) {
console.log("f2", arg);
return arg;
}
function f3(arg) {
console.log("f3", arg);
return arg;
}
function f1(arg) {
console.log("f1", arg);
return arg;
}
function f2(arg) {
console.log("f2", arg);
return arg;
}
function f3(arg) {
console.log("f3", arg);
return arg;
}
当我要使用参数 'run'
依次从 f3
到 f1
执行上图的三个函数时,一般情况会写成这样两种情况:
f1(f2(f3("run")));
f1(f2(f3("run")));
大家会发现这样虽然会按要求的顺序依次执行函数,但是同时会让代码可读性变差、不够灵活而且不够优雅。那我们不妨将“依次执行”这个处理步骤抽象出来变成一个函数,以后我只需要调用抽象出来的函数,传入我要执行的那三个函数以及参数,便可做到“依次执行”这个过程。我们来尝试一下:
function compose(...funcs) {
// funcs是我们传入的函数数组
if (funcs.length === 0) {
// 当函数数组内没有函数时,为了保持纯函数的特点,返回一个空函数
return (arg) => arg; // 为了好看,把回调参数写上去了
}
if (funcs.length === 1) {
// 当函数数组内只有一个函数时,执行函数
return funcs[0];
}
//其余情况下返回按执行顺序重组的函数数组,并将回调参数传入
return funcs.reduce(
(a, b) =>
(...args) =>
a(b(...args))
);
}
function compose(...funcs) {
// funcs是我们传入的函数数组
if (funcs.length === 0) {
// 当函数数组内没有函数时,为了保持纯函数的特点,返回一个空函数
return (arg) => arg; // 为了好看,把回调参数写上去了
}
if (funcs.length === 1) {
// 当函数数组内只有一个函数时,执行函数
return funcs[0];
}
//其余情况下返回按执行顺序重组的函数数组,并将回调参数传入
return funcs.reduce(
(a, b) =>
(...args) =>
a(b(...args))
);
}
在实现这个 compose
函数时,由于我们要传入的 f1
、f2
、f3
是函数,而 'run'
是 f1
、f2
、f3
执行时的参数,虽然他们在这个 compose
函数中所扮演的角色都是参数,但他们的含义是不一样的,为了执行时的代码易懂,我们需要对他们进行隔离,而隔离的方式就是回调函数。所以我们要这样使用 compose
:
compose(f1, f2, f3)("run");
compose(f1, f2, f3)("run");
这样我们就使用“函数的合成”将之前的代码进行了优化。对比优化前后的代码,逻辑性、可读性等大大提升。并且无论我们将来要这样执行多少个函数,只需要增加传入的函数参数即可,不用再去痛苦的“包洋葱”了。
柯里化(Currying)
柯里化(Currying),又译为卡瑞化或加里化,是把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数而且返回结果的新函数的技术。简单的说,就是把一个多参数的函数,转化为单参数函数。
我们可以想象一个这样的场景:
function add(x, y) {
return x + y;
}
add(1, 2); // 3
function add(x, y) {
return x + y;
}
add(1, 2); // 3
场景里我们实现了一个加法功能的函数,那么现在我们将其柯里化一下:
function add(y) {
return function (x) {
return x + y;
};
}
add(2)(1); //3
function add(y) {
return function (x) {
return x + y;
};
}
add(2)(1); //3
对比柯里化前后你可能会产生疑惑,不知道为什么要将函数写的这么“复杂”,那么我们看看柯里化后的函数有什么使用场景:
function add(x) {
return function (y) {
return x + y;
};
}
var increment = add(1);
var addTen = add(10);
increment(2); // 3
addTen(2); // 12
function add(x) {
return function (y) {
return x + y;
};
}
var increment = add(1);
var addTen = add(10);
increment(2); // 3
addTen(2); // 12
这个使用场景里,我们调用 add
之后,返回的函数就通过闭包的方式记住了 add
的第一个参数。而且我们还为返回的函数更改了名字,使其更“具象化”。通过这“记住参数”+“改名字”两个简单的步骤,让我们编程时将关注的重点聚焦到函数本身,而不因冗余的数据参数分散注意力。(这里仅是个人理解,如有错误还请指正)
实现一个柯里化函数
目的:实现下面的 curry 函数
function add1(x, y, z) {
return x + y + z;
}
const add = curry(add1);
console.log(add(1, 2, 3));
console.log(add(1)(2)(3));
console.log(add(1, 2)(3));
console.log(add(1)(2, 3));
function add1(x, y, z) {
return x + y + z;
}
const add = curry(add1);
console.log(add(1, 2, 3));
console.log(add(1)(2)(3));
console.log(add(1, 2)(3));
console.log(add(1)(2, 3));
实现:
const curry = (fn, ...args) =>
// 函数的参数个数可以直接通过函数数的.length属性来访问
args.length >= fn.length // 这个判断很关键!!!
? // 传入的参数大于等于原始函数fn的参数个数,则直接执行该函数
fn(...args)
: /**
* 传入的参数小于原始函数fn的参数个数时
* 则继续对当前函数进行柯里化,返回一个接受所有参数(当前参数和剩余参数) 的函数
*/
(...newArgs) => curry(fn, ...args, ...newArgs);
const curry = (fn, ...args) =>
// 函数的参数个数可以直接通过函数数的.length属性来访问
args.length >= fn.length // 这个判断很关键!!!
? // 传入的参数大于等于原始函数fn的参数个数,则直接执行该函数
fn(...args)
: /**
* 传入的参数小于原始函数fn的参数个数时
* 则继续对当前函数进行柯里化,返回一个接受所有参数(当前参数和剩余参数) 的函数
*/
(...newArgs) => curry(fn, ...args, ...newArgs);
函数式编程其实是一个完整的编程思想,这里由于时间只写了很小的一部分及理解,今后会慢慢补充!