函数式编程 -- 纯函数、柯里化函数

文章内容输出来源:拉勾教育 大前端高薪训练营

前言

学习函数式编程,要知道什么是纯函数,使用纯函数的好处,了解有关副作用的相关信息,以及纯函数相关的功能库Lodash,还有什么是函数的柯里化。

一、纯函数

1. 纯函数概念

一个函数的返回结果只依赖于它的参数,并且在执行过程里面没有副作用,我们就把这个函数叫做纯函数。

  • 纯函数的特性
  1. 相同的输入永远会得到相同的输出。
  2. 函数的返回结果只依赖于它的参数。
  3. 纯函数没有任何可观察的副作用。
  • 相同的输入永远会得到相同的输出

    纯函数就类似数学中的函数,函数将输入参数映射到返回值,也就是说,对于每套输入,都存在一个输出。

    代码如下(示例):

    const power= m => m * m;
    
  • 纯函数没有任何可观察的副作用
    一个函数执行过程对产生了外部可观察的变化,那么就说,这个函数是有副作用的。而 纯函数没有产生任何可观察的副作用,也就是说它不能改变任何外部状态。

    代码如下(示例):

    // 不纯的 
    let mini = 18 
    function checkAge (age) { 
        return age >= mini 
    }
    
    // 纯的(有硬编码,后续可以通过柯里化解决)
    function checkAge (age) { 
    	// 将 mini 变为局部变量,使外部程序观察不到,不会产生副作用
        let mini = 18  
        return age >= mini
    }
    

2. lodash功能库

lodash 是一个 JavaScript 实用工具库,提供一致性,及模块化、性能和配件等功能。它消除了处理数组的麻烦,从而简化了 JavaScript、 数字、对象、字符串等。

代码如下(示例):

// 演示 lodash
// first / last / toUpper / reverse / each / includes / find / findIndex
const _ = require('lodash')
	
const array = ['jack', 'tom', 'lucy', 'kate']

console.log(_.first(array))
console.log(_.last(array))

console.log(_.toUpper(_.first(array)))

console.log(_.reverse(array))

const r = _.each(array, (item, index) => {
  console.log(item, index)
})

console.log(r)

详情参见 Lodash 中文网

3. 纯函数的好处

  • 可缓存
    因为纯函数对相同的输入始终有相同的结果,所以可以把纯函数的结果缓存起来

    代码如下(示例):

    const _ = require('lodash') 
    function getArea (r) { 
        return Math.PI * r * r 
    }
    let getAreaWithMemory = _.memoize(getArea) 
    console.log(getAreaWithMemory(4)) 
    
  • 自己模拟一个 memoize 函数

    代码如下(示例):

    function memoize (f) { 
        let cache = {} 
        return function () { 
            let arg_str = JSON.stringify(arguments) 
            cache[arg_str] = cache[arg_str] || f.apply(f, arguments) 
            return cache[arg_str] 
        } 
    } 
    
  • 可测试
    纯函数让测试更方便

  • 并行处理
    在多线程环境下并行操作共享的内存数据很可能会出现意外情况
    纯函数不需要访问共享的内存数据,所以在并行环境下可以任意运行纯函数 (Web Worker)

4. 副作用

在上面我们讲述纯函数的特性时写到,纯函数没有任何可观察的副作用。那么,副作用的来源是什么,以及副作用将会导致什么样的影响呢?

副作用的来源

  • 配置文件
  • 数据库
  • 获取用户的输入
  • ……

所有的外部交互都有可能带来副作用,副作用也使得方法通用性下降不适合扩展和可重用性,同时副作用会给程序中带来安全隐患给程序带来不确定性,但是副作用不可能完全禁止,尽可能控制它们在可控范围内发生。

副作用的后果

副作用让一个函数变的不纯(如上例),纯函数的根据是相同的输入返回相同的输出,如果函数依赖于外部的状态就无法保证输出相同,就会带来副作用。

二、柯里化(Haskell Brooks Curry)

1. 柯里化(Currying)

维基百科上说道:柯里化(Currying),是把接受多个参数的函数变换成接受一个单一参数的函数,并且返回接受余下的参数而且返回结果的新函数的技术。

也就是说,当一个函数有多个参数的时候先传递一部分参数调用它(这部分参数以后永远不变),然后返回一个新的函数接收剩余的参数,返回结果,这就是函数的柯里化。

  • 使用柯里化解决上一个案例中硬编码的问题
    代码如下(示例):
    // 柯里化 
    function checkAge (min) { 
        return function (age) { 
            return age >= min 
        } 
    }
    
    // ES6 写法 
    let checkAge = min => (age => age >= min) 
    

2. lodash 中的柯里化函数

  • _.curry(func)
  1. 功能:创建一个函数,该函数接收一个或多个 func 的参数,如果 func 所需要的参数都被提供则执行 func并返回执行的结果。否则继续返回该函数并等待接收剩余的参数。

  2. 参数:需要柯里化的函数。

  3. 返回值:柯里化后的函数。

    代码如下(示例):

    const _ = require('lodash') 
    
    // 要柯里化的函数 
    function getSum (a, b, c) { 
        return a + b + c 
    }
    
    // 柯里化后的函数 
    let curried = _.curry(getSum) 
    
    // 测试 
    curried(1, 2, 3) // 隐藏的柯里化函数:(a, b, c) => a + b + c
    curried(1)(2)(3) // 隐藏的柯里化函数:a => b => c => a + b + c
    curried(1, 2)(3) // 隐藏的柯里化函数:(a, b) => c => a + b + c
    
  • 创建柯里化函数的通用方式
    调用另一个参数并为它传入要柯里化的函数和必要参数。

    代码如下(示例):

    // carry()函数的主要工作就是将被返回函数的参数进行排序
    // 第一个参数是要进行柯里化的函数,其他参数是要传入的值
    function curry (fn) {
    	// 获取第一个参数之后的所有参数
    	// args包含了来自外部函数的参数
    	var args = Array.prototype.slice.call(arguments, 1)
    	return function () {
    		// 存放所有传入的参数
    		var innerArgs = Array.protptype.slice.call(arguments)
    		var finalArgs = args.concat(innerArgs)
    		// 使用 apply() 将结果传递给该函数
    		return fn.apply(null, finalArgs)
    	}
    }
    
  • 模拟 _.curry() 的实现

    代码如下(示例):

    function curry (func) { 
    	// 获取函数的形参个数,可以通过 函数名.length 
    	// args 表示传进来的实际参数
    	// 此处采取有名函数,而不是匿名函数,是因为当实参和形参的个数相同时,需要调用这个函数
        return function curriedFn (...args) { // ES6语法
            // 判断实参和形参的个数
            if (args.length < func.length) { 
            	// 当传入的参数 加上 剩余参数,等于形参个数时,执行下面的代码
                return function () {  // 获取本次调用时,传入的参数
                    return curriedFn(...args.concat(Array.from(arguments))) 
                } 
            }
            // 实参大于等于形参个数时,调用 func,返回结果 
            return func(...args) // ES6语法 ... 将数组展开
        } 
    } 
    
    // 要柯里化的函数 
    function getSum (a, b, c) { 
        return a + b + c 
    }
    
    let curried = curry(getSum) 
    
    // 测试 
    // (args.length = 3) = func.length,直接调用func, 返回结果
    curried(1, 2, 3) 
    // 1. (args.length = 1) < func.length , 执行匿名函数,调用curriedFn,传入参数2,此时 args.length = 2
    // 2. (args.length = 2) < func.length, 执行匿名函数,调用curriedFn,传入参数3,此时 args.length = 3
    // 3. (args.length = 3) = func.length,直接调用func, 返回结果
    curried(1)(2)(3) 
    // 1. (args.length = 2) < func.length , 执行匿名函数,调用curriedFn,传入参数3,此时 args.length = 3
    // 2. (args.length = 3) = func.length,直接调用func, 返回结果
    curried(1, 2)(3) 
    

3. 总结

  • 柯里化可以让我们给一个函数传递较少的参数得到一个已经记住了某些固定参数的新函数;
  • 这是一种对函数参数的’缓存’;
  • 让函数变的更灵活,让函数的粒度更小;
  • 可以把多元函数转换成一元函数,可以组合使用函数产生强大的功能。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值