前言
在学习函数式编程之前,我们先来说说为什么要学习它,主要有一下几方面原因:
- 提升代码质量,让别人对你刮目相看
- 可以作为面试讨论的资本
- 让你可以读得懂更加高级的代码
针对第三点说明一下,我在阅读redux
中间件源码时,发现内部
使用了compose
,我一下子懵了,这是什么东西,学习了函数式编程之后,发现它不过如此。
函数式编程定义
函数式编程必须满足2个特性:
1.引用透明性或纯函数
即对于函数相同的输入都将返回其相同的输出,意思就是函数传入一个参数必须返回经过计算后得到的数据,且不依赖外部环境,例:
const double = (i) => 2*i
// 返回4
double(2)
2.用声明式代码不用命令式代码
// 命令式代码
const arr = [1, 2, 3]
for (let i = 0; i< arr.length; i++) {
console.log(arr[i])
}
// 声明式代码
const arr = [1, 2, 3]
arr.forEach(value => {
console.log(value)
})
意思就是声明式代码是命令式代码的能力的封装,使用起来更简单优雅一些。下面我们来看下forEach
是如何实现的,利用声明式的思想去封装forEach
。
// ~util.js
const forEach = (array,fn) => {
let i;
for(i=0;i<array.length;i++)
fn(array[i])
}
export {
forEach,
}
// ~index.js
import { forEach } from 'util'
const arr = [1, 2, 3]
forEach(arr, value => {
console.log(value)
})
高阶函数
高阶函数是接收另一个函数作为参数或返回一个函数的函数,注意这里是或,不是且,也就是说只要函数作为参数传入就是高阶函数,或直接返回一个函数也是高阶函数。下面我们来看一个高阶函数的例子:
// ~util.js
const sortBy = (property) => {
return (a,b) => {
const result = (a[property] < b[property]) ? -1 : (a[property] > b[property]) ? 1 : 0;
return result;
}
}
// ~index.js
import { sortBy } from 'util'
const people = [
{firstname: "aaFirstName", lastname: "cclastName"},
{firstname: "ccFirstName", lastname: "aalastName"},
{firstname:"bbFirstName", lastname:"bblastName"}
];
console.log("通过firstname排序", people.sort(sortBy("firstname")))
console.log("通过lastname排序", people.sort(sortBy("lastname")))
sortBy
直接返回一个函数,因此sortBy
是高阶函数,sort
以一个函数作为参数,因此sort
是高阶函数。如果没有sortBy
函数,每次都要重新定义比较函数,这就降低了代码重用性。
闭包与高阶函数
首先我们来回顾一下什么是闭包,简单来说就是函数里边嵌套函数,请看以下代码:
function outer() {
function inner(){
}
}
这就是闭包,其中inner
称为闭包函数,那闭包的作用是什么呢?请看以下代码:
const outer = (arg) => {
const hello = 'hello';
return () => {
console.log(`${hello} ${arg}`)
}
}
const print = outer('dengshangli')
// 输出 hello dengshangli
print();
pirnt
就是outer
返回的函数,调用后发现它缓存了第一次传入的参数arg
以及在outer
里边定义的变量hello
,说明闭包函数具有缓存传入参数以及外部函数变量的功能。outer
函数返回了一个函数,根据我们前面的定义,说明它除了是闭包以外还是一个高阶函数,接下来我们来看2个闭包的例子:
1.once
函数
once
函数的作用是让函数只执行一次,我们来看看是如何实现的:
// ~util.js
const once = (fn) => {
let done = false;
return function () {
return done ? undefined : ((done = true), fn.apply(this, arguments))
}
}
// ~index.js
import { once } from 'util'
const testOnce = once((arg) => {
console.log(arg)
})
// 打印出dengshangli
testOnce('dengshangli');
// 再执行一次,返回undefined
testOnce('dengshangli');
在once
里边定义了变量done
,用于判断函数是否执行过,由于once
是一个闭包,因此done
可以被缓存,当第二次testOnce
时判断已被执行过,一次不会再执行。·闭包函数使用apply
的原因是恢复函数执行环境。
2.memoized
函数
memoized
函数的功能是让函数带有记忆功能,举一个例子,我们要做是计算一个数的阶乘,没有memoized
的时候,每次都要重新计算,有了memoized
,就可以缓存上次计算的结果,下次计算直接使用,从而减少了计算量,具体实现如下:
// ~util.js
const memoized = (fn) => {
const lookupTable = {};
return (arg) => lookupTable[arg] || (lookupTable[arg] = fn(arg));
}
// ~index.js
import { memoized } from 'util'
//递归计算阶乘
const factorial = (n) => {
if(n === 0) {
return 1
}
return n*factorial(n-1)
}
const fastFactorial = memoized(factorial)
// 第一次计算输出120
fastFactorial(5)
// 第二次计算输出720
fastFactorial(6)
memoized
函数是一个闭包,里边定义了变量lookupTable
,用于缓存上一次计算的结果,下次调用时如果存在直接从里边取,从而节省了计算的时间。
数组的函数式编程
原生数组支持已经支持forEach、map、filter
等函数,都是通过函数式编程的思想去实现的,这里我们没有必要再实现一遍,这里我们重点介绍zip
函数,这个函数的作用是用来连接2个数组,例:
const arr1 = [
{id: 1, name:'1'},
{id: 2, name:'2'},
]
const arr2 = [
{id: 1, age:'1'},
{id: 2, age:'2'},
]
//我们想要得到以下数组
const arr3 = [
{id: 1, name:'1', age:'1'},
{id: 2, name:'2', age:'2'},
]
这种情况可能会出现在后端返回给我们的数据可能是2个接口得到的,我们想要得到形如arr3
的数据,就可以通过zip
函数实现。
// ~util.js
const zip = (leftArr,rightArr,fn) => {
let index, results = []
for(index = 0;index < Math.min(leftArr.length, rightArr.length);index++)
results.push(fn(leftArr[index],rightArr[index]))
return results
}
// ~index.js
import { zip } from 'utils'
const arr1 = [
{id: 1, name: '1'},
{id: 2, name: '2'},
]
const arr2 = [
{id: 1, age: '1'},
{id: 2, age: '2'},
]
const arr3 = zip(arr1, arr2, (arr1Item, arr2Item) => {
if (arr1Item.id === arr2Item.id) {
return { ...arr1Item, ...arr2Item }
}
})
console.log(arr3)
通过zip
函数成功连接了2个数组,Math.min(leftArr.length, rightArr.length)
表示只遍历长度最小的那个,因为长度长的那个剩余部分没有连接的必要。
柯里化与偏应用
1.柯里化
柯里化是把多元函数转换成一个嵌套的一元函数的过程,简单的说就是把带有多个参数的函数,通过闭包缓存,转换成只有一个参数的嵌套函数,请看以下代码:
// ~util.js
const curry = (binaryFn) => {
return function (firstArg) {
return function (secondArg) {
return binaryFn(firstArg, secondArg);
};
};
};
// ~index.js
import { curry } 'util'
const add = (x, y) => x + y
// 将二元函数转为一元函数
const addCurried = curry(add)
// 输出8
console.log(addCurried(4)(4))
curry
就是柯里化函数,通过闭包缓存第一个参数及第二个参数,然后执行真正执行逻辑的函数,然后我们通过一个加法函数去测试,最后成功输出了结果。但是我们发现curry
只能处理2个参数的情况,那多个参数的情况该怎么办呢?请看以下代码:
// ~util.js
const curryN =(fn) => {
if(typeof fn!=='function'){
throw Error('No function provided');
}
return function curriedFn(...args){
// 传入参数长度小于传入函数参数长度时才执行
if(args.length < fn.length){
return function(){
// [].slice.call(arguments)表示将类数组转为真实数组
return curriedFn.apply(null, args.concat( [].slice.call(arguments) ));
};
}
return fn.apply(null, args);
};
};
// ~index.js
import { curryN } 'util'
const add = (x, y, z) => x + y + z
// 将多元函数转为一元函数
const addCurried = curryN(add)
// 输出12
console.log(addCurried(4)(4)(4))
// 输出12
console.log(addCurried(4, 4)(4))
我们来分析以下代码,curryN
内部递归执行了curriedFn
,当我们执行addCurried(4)
时,就等于执行curriedFn(4)
,返回一个function
,执行addCurried(4)(4)
等于执行function(4)
,此时arguments
为{0: 4}
,args
为[4]
,执行curriedFn(4, 4)
,再返回一个function
,以此类推,直到执行完毕为止,最后将累加得到的arg
传给最初包装的add
函数,得到执行结果。返回curryN
要比curry
强大得多,它不仅可以将多元函数转换为一元函数,如果中间出现二元的情况情况它也是可以处理的,这点体现在addCurried(4, 4)(4)
。再来看一个柯里化的实例:
const loggerHelper = (mode,initialMessage,errorMessage,lineNo) => {
if(mode === "DEBUG")
console.debug(initialMessage,errorMessage + "at line: " + lineNo)
else if(mode === "ERROR")
console.error(initialMessage,errorMessage + "at line: " + lineNo)
else if(mode === "WARN")
console.warn(initialMessage,errorMessage + "at line: " + lineNo)
else
throw "Wrong mode"
}
let errorLogger = curryN(loggerHelper)("ERROR")("Error At Stats.js");
let debugLogger = curryN(loggerHelper)("DEBUG")("Debug At Stats.js");
let warnLogger = curryN(loggerHelper)("WARN")("Warn At Stats.js");
//for error
errorLogger("Error message",21)
//for debug
debugLogger("Debug message",233)
//for warn
warnLogger("Warn message",34)
curryN
将各种错误类型缓存起来,用的时候直接调用errorLogger
,是不是比之前传入多个参数简洁多了。
2.偏应用
偏应用技术是指利用闭包缓存函数的部分参数,将某个可变的参数保留到函数真正调用时传入,请看以下代码:
// ~util.js
const partial = function (fn,...partialArgs){
let args = partialArgs.slice(0);
return function(...fullArguments) {
let arg = 0;
for (let i = 0; i < args.length && arg < fullArguments.length; i++) {
if (args[i] === undefined) {
args[i] = fullArguments[arg++];
}
}
return fn.apply(this, args);
};
};
// index.js
import { partial } from 'util'
const obj = { foo: 'foo', bar: 'bar' }
JSON.stringify(obj, null, 2)
const prettyPrintJson = partial(JSON.stringify, undefind, null, 2)
prettyPrintJson(obj)
JSON.stringify(obj, null, 2)
的意思是返回格式化后的JSON
字符串,但参数null, 2
是不变的,每次都这么调用显得不够优雅,我们希望借助偏函数缓存这2个参数,同时让obj
在真正执行函数时传入。于是我们在用偏函数包裹时JSON.stringify
传入undefind
,在partial
中发现最终undefind
被替换成了obj
。在每次希望执行格式化是执需prettyPrintJson(obj)
,非常优雅。
组合与管道
1.管道的概念
管道的概念来自Unix
,通过|
来连接前后内容,|
被称为管道符号,前面的输出将作为后边的输入,请看以下代码:
cat test.txt | grep 'world' | wc
cat
用于读取test.txt
文本内容,假如test.txt
里边是hello world
,控制台将输出hello wrold
,grep
接收cat
的输出,并且返回与world
关联的内容,wc
计算给定单词在文本中的数量。可以看出,通过管道符号会把前一次计算的结果通过参数传给下一个命令。
2.compose函数
compose
函数就是函数组合函数,借助管道的概念,接收多个函数作为参数,从最右边的参数开始执行,把执行的结果返回给下一个函数,作为下一个函数的参数,直到执行到最左边的函数为止,请看以下代码:
// ~util.js
// 同数组中的reduce一致
const reduce = (array, fn, initialValue) => {
let accumlator;
if (initialValue != undefined)
accumlator = initialValue;
else
accumlator = array[0];
for (const value of array)
accumlator = fn(accumlator, value)
return accumlator
}
const compose = (...fns) => (value) => {
return reduce(fns.reverse(), (acc, fn) => fn(acc), value);
}
// ~index.js
import { compose } from 'utils'
// 将字符串通过空格转换为数组
let splitIntoSpaces = (str) => str.split(" ");
// 统计数组的长度
let count = (array) => array.length;
// 判断数字是基数还是偶数
let oddOrEven = (ip) => ip % 2 == 0 ? "even" : "odd"
// 组合三个函数, 并且让最后一个函数暂时不执行
const oddOrEvenWords = compose(oddOrEven,count,splitIntoSpaces);
// compose满足结合律,我们还可以这么写
// var oddOrEvenWords = compose(oddOrEven,compose(count,splitIntoSpaces));
// 输出odd
console.log(oddOrEvenWords("hello your reading about composition"))
compose
函数非常简单,只有一行代码,借助reduce
函数,把上一个函数执行的结果传给下一个函数。与之对应的pipe
,它的功能与compose
一模一样,只不过它的参数执行顺序是从左向右,而不是从右向左,我们只需把compose
中的fns.reverse()
变为fns
即可。
3.compose函数调试技巧
const showParams = (params) => {
console.log(params)
return params
}
const oddOrEvenWords = compose(oddOrEven,showParams,count,splitIntoSpaces);
通过在中间传入showParams
函数,就可以在控制台打印出前一次执行的结果,以便于快速定位错误。
函子
1.函子的概念
const MayBe = function(val) {
this.value = val
}
MayBe.of = function(val) {
return new MayBe(val)
}
//MayBe.prototype.isNothing = function() {
// return this.value === null || this.value === undefined
//}
MayBe.prototype.map = function(fn) {
return MayBe.of(fn(this.value))
}
const double = (x) => 2*x
// 返回MayBe {value: 12}
MayBe.of(3).map(double).map(double)
具有以上结构的函数被称为涵子,其中map
是可以通过链式调用的。
const MayBe = function(val) {
this.value = val
}
MayBe.of = function(val) {
return new MayBe(val)
}
MayBe.prototype.isNothing = function() {
return this.value === null || this.value === undefined
}
MayBe.prototype.map = function(fn) {
return this.isNothing ? MayBe.of(null) : MayBe.of(fn(this.value))
}
const double = (x) => 2*x
// 返回MayBe {value: null}
MayBe.of(null).map(double).map(double)
我们在MayBe
函数的原型链上新增一个isNothing
函数,用于判断当前值是否为null
或undefined
,如果是就返回true
,并且在map
里边调用了它,如果返回true
就调用MayBe.of(null)
将当前值设为null
,以后无论调用多少次map
,所传的函数都不会执行了值到最终都是null
。由此可见MayBe
可以用来处理在编码中遇到的传值错误的情况,而不是将错误直接抛给浏览器,同时还可以对值进行一些操作,例如使用double
函数。
总结
以上就是函数是编程的所有内容了,相信很多函数大家在平时开发中都遇到过,这些都不是很高深的东西,但是如果你没有学习过,就不知道这些原来这就是函数式编程的内容,希望这篇文章能帮助大家理解函数式编程的一些概念及用法。