数组的函数式方法
当程序员谈到函数式编程时,基本都会提到三个数组相关的函数方法:map
filter
reduce
,综合利用它们可以实现一个完整的数据流动和处理 。其中map
和filter
被称为投影函数,即将一个函数作用于数组上并生成一个新的数组。
注意:
forEach
和map
都是遍历数组,forEach
函数是作用于数组,并不会返回新的数组,但是map
会返回一个新数组。
map
遍历数组,按照给定的函数执行,并储存并返回生成的新数组。- 不改变原数组
filter
遍历数组,按照给定的函数筛选数组,返回符合要求的值的数组。- 不改变原数组
reduce
从前往后遍历数组,按照给定的函数**“整合”**数组,返回一个结果值。- 不改变原数组
在JavaScript中,其实已经有相关的数组内置方法实现,例如 Array.map
Array.filter
Array.reduce
Array.reduceRight
等。在日常开发时可以直接使用,不必自己实现,除非有其它特殊需求。
然而这些函数可以脱离数组的束缚,有利于函数式编程
const map = (arr, func) => {
let ans = [];
for (let a of arr) ans.push(func(a));
return ans;
}
const filter = (arr, func) => {
let ans = [];
for (let a of arr) {
if (func(a)) {
ans.push(a);
}
}
return ans;
}
const reduce = (arr, func, initial) => {
let start = 0;
if (!initial) {
initial = arr[0];
start++;
}
for (let i = start; i < arr.length; i++) {
initial = func(initial, arr[i])
}
return initial;
}
柯里化 currying
柯里化是把一个多参数函数转化为一个嵌套的一元函数的过程。
举个简单的例子:将 add 函数分解
// 多参数函数写法
const add = (x, y) => x + y; // add(2, 3)
// 嵌套的一元函数,即柯里化
const addCurry = x => y => x + y; // addCurry(2)(3)
或者将其抽离出来:
const curry = func => x => y => func(x, y);
let addCurry = curry(add); // addCurry(2)(3)
使用:
const temp = addCurry(10);
temp(2); // 12
temp(10); // 20
柯里化可以将一个多参数函数分解成一个一步步往下走的链状函数,类似于数据流:
let curryedFunc = curryn(func);
let sorted = curryedFunc(param);
let filtered = sorted(param2);
let maped = filtered(param3);
let ans = maped(param4);
// or
// ans = curryedFunc(param)(param2)(param3)(param4);
利用柯里化还可以缓存结果,有利于复用。比如:
let isEven = f => arr => arr.filter(f);
let findEven = isEven(i => i % 2 == 0);
findEven([2, 5, 6])
// => [ 2, 6 ]
findEven([11, 123, 98])
// => [98]
打印日志
将柯里化应用到日志打印是常见的用法,如下,声明一个函数,分类输出不同类别的日志:
const logger = (type, info, error, line) => {
if (type === 'error') {
console.error(`${info}, error: ${error} line at: ${line}`)
} else if (type === 'warning') {
console.error(`${info}, warning: ${error} line at: ${line}`)
} else if (type === 'debug') {
console.error(`${info}, debug: ${error} line at: ${line}`)
} else { // 'log'
console.log(info)
}
}
可以利用柯里化抽象一下,更方便的调试输出:
const log = info => logger('log', info);
const error = info => error => line => logger('error', info, error, line);
const warn = info => error => line => logger('warning', info, error, line);
const debug = info => error => line => logger('debug', info, error, line);
调用方法如下:
log("log");
error('info')('error')(2);
warn('info')('warn')(3);
debug('info')('debug')(4);
柯里化函数
如上所示,我们需要自行判断函数的参数个数,然后抽象实现其特定的柯里化函数,失去了一般性。我们可以利用递归来实现一个通用的、自行判断参数个数的抽象柯里化函数,类似于 rambda
库中的 curryn()
函数。
const curryn = func => {
return function curryFunc(...args) {
if (args.length < func.length) {
return (...params) => {
return curryFunc.apply(null, args.concat([...params]));
}
}
return func.apply(null, args);
}
}
回顾上面的日志函数,现在我们可以使用通用方法 curryn
将其柯里化:
let error = curryn(logger)
error('error')('info')('erro info')(3); // 'info, error: erro info line at: 3'
你甚至还可以使用多参数的调用方法:
e('error', 'info', 'erro info', 3); // 'info, error: erro info line at: 3'
或者,混用!
e('error', 'info')('erro info')(3); // 'info, error: erro info line at: 3'
柯里化是不是很神奇!
高级的柯里化同时容许 函数正常调用 和 获取偏函数!
偏函数
在上面实现柯里化函数的时候,你可能注意到了,参数我们默认是从左往右的数据流动方向,首先缓存的是左边的参数。但是当我们需要保存右边的参数实现复用时,就遇到了麻烦。例如,reduce函数:
reduce((previousValue, currentValue) => { /* … */ } , initialValue)
如果我们要缓存 initialValue
参数的值来进行复用该怎么办?
也许,聪明的你会想到,将其重新封装一下不就好了!
const reduceWrapper = (func, initialValue, arr) => reduce(arr, func, initialValue);
let reduceAddTwo = curryn(reduceWrapper)((p, c) => p + c)(2);
reduceAddTwo([1, 3]);
// => 6
reduceAddTwo([2, 4]);
// => 8
但是的话这样必须消耗一个封装器,为了避免多余的消耗,我们可以抽象一个偏函数 (partial)来实现。
注意:偏函数并不是意味着和柯里化“相反”,偏函数是指将一些参数固定到一个函数,产生另外一个较小的函数的过程,即部分地应用函数参数(提前将某些参数放进去)。
函数在柯里化以后,逐个传递参数的时候返回的那一层封装其实就是函数的偏函数变体。
柯里化的目的 就是在不影响_初始函数_的调用形式的状况下,更方便的得到初始函数的 偏函数 变体。
严格来讲,我们上文好几个函数都能算是偏函数,但大都是提前将左边的参数缓存。下面给出先缓存右边参数的实现:
const partial = func => {
return function partialFunc(...args) {
if (args.length < func.length) {
return (...params) => {
return partialFunc.apply(null, [...params, ...args]);
}
}
return func.apply(null, args);
}
}
let reduceAddThree = partial(reduce)((p, c) => p + c, 3);
reduceAddThree([3, 3, 9])
// => 18
reduceAddThree([1, 2])
// // => 6