前端进阶之路二-闭包、纯函数、柯里化

409 阅读5分钟

闭包

闭包 (Closure):函数和其周围的状态(词法环境)的引用捆绑在一起形成闭包。可以在另一个作用域中调用一个函数的内部函数并访问到该函数的作用域中的成员 

// 函数作为返回值 function makeFn () {    let msg = 'Hello function' }const fn = makeFn() fn() 
// 正常情况下,调用makeFn这个函数,调用完后函数中的变量msg会被释放掉


// 函数作为返回值 function makeFn () { let msg = 'Hello function'   return function () {     console.log(msg) // 内部函数引用外部函数变量msg  } }const fn = makeFn() fn()// 当内部函数对外部函数变量有引用,则变量msg不能被释放,形成闭包

// once function once (fn) { let done = false    return function () {      if (!done) {         done = true         return fn.apply(this, arguments)      }    } }

let pay = once(function (money) {    console.log(`支付:${money} RMB`) })

// 只会支付一次 pay(5) pay(5) 

闭包的本质:函数在执行的时候会放到一个执行栈上当函数执行完毕之后会从执行栈上移除,但是堆上的作用域成员因为被外部引用不能释放,因此内部函数依然可以访问外部函数的成员

纯函数

概念: 相同的输入永远会得到相同的输出,而且没有任何可观察的副作用

lodash

是一个纯函数的功能库,提供了对数组、数字、对象、字符串、函数等操作的一些方法 

函数式编程不会保留计算中间的结果,所以变量是不可变的(无状态的)

我们可以把一个函数的执行结果交给另一个函数去处理

命令行:npm install lodash

案例

数组的 slice 和 splice 分别是:纯函数和不纯的函数

slice 返回数组中的指定部分,不会改变原数组

splice 对数组进行操作返回该数组,会改变原数组

let numbers = [1, 2, 3, 4, 5] // 纯函数 numbers.slice(0, 3) // => [1, 2, 3] numbers.slice(0, 3) // => [1, 2, 3] numbers.slice(0, 3) // => [1, 2, 3] // 不纯的函数 numbers.splice(0, 3) // => [1, 2, 3] numbers.splice(0, 3) // => [4, 5] numbers.splice(0, 3) // => [] 

纯函数的好处

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

const _ = require('lodash')    function getArea (r) {    return Math.PI * r * r }let getAreaWithMemory = _.memoize(getArea) console.log(getAreaWithMemory(4)) 

2、可测试

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

副作用 

// 不纯的 let mini = 18 // 该变量声明在函数体外部,当值发生改变时,checkAge函数无法保证输出相同结果function checkAge (age) {    return age >= mini }// 纯的(有硬编码,后续可以通过柯里化解决) function checkAge (age) {    let mini = 18    return age >= mini } 

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

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

柯里化 (Haskell Brooks Curry) 


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

function checkAge (age) {    let min = 18    return age >= min }// 普通纯函数 function checkAge (min, age) {    return age >= min }checkAge(18, 24) checkAge(18, 20) checkAge(20, 30) // 柯里化 function checkAge (min) {    return function (age) {       return age >= min    } }// ES6 写法 let checkAge = min => (age => age >= min) let checkAge18 = checkAge(18) let checkAge20 = checkAge(20) checkAge18(24) checkAge18(20)

lodash 中的柯里化函数 

_.curry(func)

功能:创建一个函数,该函数接收一个或多个 func 的参数,如果 func 所需要的参数都被提

供则执行 func 并返回执行的结果。否则继续返回该函数并等待接收剩余的参数。

参数:需要柯里化的函数 

返回值:柯里化后的函数

const _ = require('lodash') // 要柯里化的函数 function getSum (a, b, c) {    return a + b + c }// 柯里化后的函数 let curried = _.curry(getSum) // 测试 curried(1, 2, 3) curried(1)(2)(3) curried(1, 2)(3) 

 案例

// 柯里化案例
// ''.match(/\s+/g)
// ''.match(/\d+/g)

const _ = require('lodash')

const match = _.curry(function (reg, str) {
  return str.match(reg)
})

const haveSpace = match(/\s+/g)
const haveNumber = match(/\d+/g)

const filter = _.curry(function (func, array) {
  return array.filter(func)
})

const findSpace = filter(haveSpace)

// console.log(haveSpace('helloworld'))
// console.log(haveNumber('abc'))

console.log(filter(haveSpace, ['John Connor', 'John_Donne']))


console.log(findSpace(['John Connor', 'John_Donne']))

模拟 _.curry() 的实现

// 模拟实现 lodash 中的 curry 方法

function getSum (a, b, c) {
  return a + b + c
}

const curried = curry(getSum)

console.log(curried(1, 2, 3))
console.log(curried(1)(2, 3))
console.log(curried(1, 2)(3))


function curry (func) {
  return function curriedFn(...args) {
    // 判断实参和形参的个数
    if (args.length < func.length) {
      return function () {
        return curriedFn(...args.concat(Array.from(arguments)))
      }
    }
    return func(...args)
  }
}

总结 

1、柯里化可以让我们给一个函数传递较少的参数得到一个已经记住了某些固定参数的新函数

2、这是一种对函数参数的'缓存'

3、让函数变的更灵活,让函数的粒度更小

4、可以把多元函数转换成一元函数,可以组合使用函数产生强大的功能