学习JavaScript函数式编程

前言

在学习函数式编程之前,我们先来说说为什么要学习它,主要有一下几方面原因:

  • 提升代码质量,让别人对你刮目相看
  • 可以作为面试讨论的资本
  • 让你可以读得懂更加高级的代码

针对第三点说明一下,我在阅读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 wroldgrep接收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函数,用于判断当前值是否为nullundefined,如果是就返回true,并且在map里边调用了它,如果返回true就调用MayBe.of(null)将当前值设为null,以后无论调用多少次map,所传的函数都不会执行了值到最终都是null。由此可见MayBe可以用来处理在编码中遇到的传值错误的情况,而不是将错误直接抛给浏览器,同时还可以对值进行一些操作,例如使用double函数。

总结

以上就是函数是编程的所有内容了,相信很多函数大家在平时开发中都遇到过,这些都不是很高深的东西,但是如果你没有学习过,就不知道这些原来这就是函数式编程的内容,希望这篇文章能帮助大家理解函数式编程的一些概念及用法。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值