函数式编程的一些理解

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

1. 什么是函数式编程

  • 函数式编程,缩写FP,是一种编程范式,也是一种编程风格,亦或是一种思维模式,和面向对象是并列的关系,面向对象是对事物的抽象,函数式是对运算过程的抽象
  • 函数式编程不是指的函数Function,而是指的数学中的函数对应关系,比如:y = sin(x),y和x的关系,相同的x下永远会得到相同的y(纯函数)

2. 高阶函数

需要一个函数作为参数,屏蔽细节,不用关心实现细节,可以用来抽象通用问题

2.1 函数作为参数
// 模拟forEach
function forEach (arr, fn) {
  for (let i = 0; i < arr.length; i += 1) {
    fn(arr[i], i)
  }
}

const arr = [1, 3, 5, 7, 9]

// 测试forEach
forEach(arr, function (item, index) {
  console.log(item, index)
})

// 模拟filter
function filter (arr, fn) {
  let results = [];
  for (let i = 0; i < arr.length; i += 1) {
    if (fn(arr[i])) {
      results.push(arr[i])
    }
  }
  return results
}

// 测试filter
const r = filter(arr, function (item) {
  return item % 3 === 0
})
console.log(r)
2.2 函数作为返回值
function makeFn () {
  let msg = 'hello world'
  return function () {
    console.log(msg)
  }
}

const fn = makeFn()
fn()
makeFn()() // hello world

// once函数
function once (fn) {
  let flag = true
  return function () {
    if (flag) {
      flag = false
      return fn.apply(this, arguments)
    }
  }
}

const r = once(function (arg) {
  console.log(arg)
})
r(22) // 22
r(22) // 不执行
r(22) // 不执行
2.3 模拟常用的高阶函数
// 模拟高阶函数 map、every、some
let arr = [1, 2, 3]

const map = (arr, fn) => {
  let results = []
  for (let value of arr) {
    results.push(fn(value))
  }
  return results
}
const fMap = map(arr, v => v * v)
console.log(fMap)

const every = (arr, fn) => {
  let result = true
  for (let value of arr) {
    result = fn(value)
    if (!result) {
      break
    }
  }
  return result
}
const fEvery = every(arr, v => v >= 1)
console.log(fEvery)

const some = (arr, fn) => {
  let result = false
  for (let value of arr) {
    result = fn(value)
    if (result) {
      break
    }
  }
  return result
}
const fSome = some(arr, v => v % 2 === 0)
console.log(fSome)

3. 闭包

闭包就是能够读取其他函数内部变量的函数,内层函数引用了外层函数作用域下的变量,并且内层函数在全局下可访问

function makeSalary (base) {
  return function (performance) {
    return base + performance
  }
}
let salaryLevel1 = makeSalary(12000)
let salaryLevel2 = makeSalary(15000)

console.log(salaryLevel1(3000)) // 15000
console.log(salaryLevel1(5000)) // 17000
console.log(salaryLevel2(5000)) // 20000

4. 纯函数

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

const arr = [1, 2, 3, 4, 5]
// 纯函数slice
console.log(arr.slice(0, 3))
console.log(arr.slice(0, 3))
console.log(arr.slice(0, 3))

// 不纯的函数splice,改变了原数组
console.log(arr.splice(0, 3))
console.log(arr.splice(0, 3))
console.log(arr.splice(0, 3))

// 纯函数
function getSum (a, b) {
  return a + b
}
console.log(getSum(1, 2))
console.log(getSum(1, 2))
console.log(getSum(1, 2))

5. lodash和loadsh/fp

  • lodash是一个函数功能库,提供了对数组、数字、对象、字符串、函数等操作的一些方法
  • loadsh/fp提供了函数式编程模块
  • loadsh/fp提供了不可变auto-curried iteratee-first data-last的方法
  • lodash是数据优先,函数置后;lodash/fp是函数优先,数据置后
const _ = require('lodash')
const fp = require('lodash/fp')

// lodash方法
const r1 = _.map(['a', 'b', 'c'], _.toUpper)

// lodash/fp方法
const r2 = fp.map(fp.toUpper, ['a', 'b', 'c'])
const r3 = fp.map(fp.toUpper)(['a', 'b', 'c'])

console.log(r1)
console.log(r2)
console.log(r3)

const r4 = fp.flowRight(fp.join('-'), fp.map(fp.toUpper), fp.split(' '))
console.log(r4('hello my world'))

// _和fp map方法的区别

console.log(_.map(['23', '8', '10'], parseInt))
console.log(fp.map(parseInt, ['23', '8', '10']))

// _.map中接受三个参数,value, index/key, collection
// parseInt第二个参数为进制,相当于如下执行,0为10进制,1不存在,2为2进制,最后输出[ 23, NaN, 2 ]
// parseInt('23', 0, ['23', '8', '10'])
// parseInt('8', 1, ['23', '8', '10'])
// parseInt('10', 2, ['23', '8', '10'])

6. 柯里化

为了解决硬编码问题,把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数而且返回结果的新函数,可以让函数变得更灵活,使用场景更广泛

const _ = require('lodash')

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 (fn) {
  const fnLen = fn.length
  return function _curry (...args) {
    if (args.length < fnLen) {
      return function () {
        return _curry(...args.concat(Array.from(arguments)))
      }
    }
    return fn(...args)
  }
}
const match = curry(function (reg, str) {
  return str.match(reg)
})
const haveSpace = match(/\s+/g)
const haveNumber = match(/\d+/g)
const filter = curry(function (fn, arr) {
  return arr.filter(fn)
})
const findSpace = filter(haveSpace)
console.log(findSpace(['d_ddd', 'd dd']))

7. 函数组合

函数组合可以让我们把细粒度的函数重新组合生成一个新的函数,避免写出洋葱代码

作用:

  • 单一功能的小函数更好维护
  • 通过组合,将单一功能的小函数串联起来,完成复杂的功能
  • 复用性更好,硬编码更少

模拟函数组合

const _ = require('lodash')

function compose (...funcs) {
  return function (val) {
    return funcs.reverse().reduce(function (acc, fn) {
      return fn(acc)
    }, val)
  }
}

const setArrFirstUpper = compose(_.toUpper, log('first'), _.first, log('reverse'), _.reverse)
const r = setArrFirstUpper(['aaa', 'bbb', 'ccc'])
console.log(r) 

// 函数组合结合律,任意组合函数组合内的函数,结果都一样
const setArrFirstUpper1 = compose(compose(_.toUpper, log('first'), _.first), log('reverse'), _.reverse)
const r1 = setArrFirstUpper1(['aaa', 'bbb', 'ccc'])
console.log(r1)

8. point free

  • point free 是指一种函数式的编程风格,实现方式实际是函数组合
  • 不需要指明处理的数据,只需要合成运算过程,需要定义一些辅助的基本运算函数
const fp = require('lodash/fp')

// 非point-free模式
function f (word) {
  return word.toLowerCase().replace(/\s+/g, '-')
}

// point-free模式
const pf = fp.flowRight(fp.replace(/\s+/g, '-'), fp.toLower)

const word = 'HELLO   woRD'
console.log(f(word))
console.log(pf(word))

9. 函子(Functor)

容器:包含值和值的变形关系(这个变形关系就是函数)
函子:是一个特殊的容器,通过一个普通的对象来实现,该对象具有map方法,map方法可以运行一个函数对值进行处理(变形关系)

  • 函数式编程的运算不直接操作值,而是由函子完成
  • 函子就是一个实现来map契约的对象
  • 我们可以把函子想象成一个盒子,这个盒子里封装了一个值
  • 想要处理盒子中的值,我们需要给盒子的map方法传递一个处理值的函数(纯函数),由这个函数来对值进行处理
  • 最终map方法返回一个包含新值的盒子(函子)

几种常见的函子

/* MayBe函子,对外部空值进行处理 */
class MayBe {
  static of (value) {
    return new MayBe(value)
  }

  constructor (value) {
    this._value = value
  }

  map (fn) {
    return this.isNothing() ? MayBe.of(null) : MayBe.of(fn(this._value))
  }

  isNothing (value) {
    return this._value === null || undefined
  }
}

console.log(MayBe.of(null).map(x => x.toUpperCase()))
console.log(MayBe.of('null').map(x => x.toUpperCase()))

/* Either函子类似于if...else...,异常会让函数变得不纯,Either函子可以用来做异常处理 */
class Left {
  static of (value) {
    return new Left(value)
  }

  constructor (value) {
    this._value = value
  }

  map () {
    return this
  }
}

class Right {
  static of (value) {
    return new Right(value)
  }

  constructor (value) {
    this._value = value
  }

  map (fn) {
    return Right.of(fn(this._value))
  }
}

function parseJson (str) {
  try {
    return Right.of(JSON.parse(str))
  } catch (e) {
    return Left.of({ error: e.message })
  }
}

console.log(parseJson('{ name: zs }'))
console.log(parseJson('{ "name": "zs" }'))

/* 
  IO函子
  IO函子中的_value是一个函数,这里把函数作为值来处理
  IO函子可以把不纯的动作存储到_value中,延迟执行这个不纯的操作(惰性执行),包装当前的操作
  把不纯的操作交给调用者来处理
*/
class IO {
  static of (value) {
    return new IO(function () {
      return value
    })
  }

  constructor (fn) {
    this._value = fn
  }

  map (fn) {
    return new IO(fp.flowRight(fn, this._value))
  }
}

console.log((IO.of(process).map(p => p.execPath))._value())
// IO函子存在嵌套函子调用的不方便,._value()._value()
const readFileIO = function (filename) {
  return new IO(function () {
    return fs.readFileSync(filename, 'utf-8')
  })
}
const print = function (x) {
  return new IO(function () {
    console.log(x)
    return x
  })
}

const cat = fp.flowRight(print, readFileIO)
console.log(cat('package.json')._value()._value())
/*
  解决IO函子嵌套问题:Monad函子
  可以变扁的Pointed函子,IO(IO(x))
  一个函子如果具有join和of两个方法并遵守一些定律就是一个Monad
*/
class IOMonad {
  static of (value) {
    return new IOMonad(function () {
      return value
    })
  }

  constructor (fn) {
    this._value = fn
  }

  map (fn) {
    return new IOMonad(fp.flowRight(fn, this._value))
  }

  join () {
    return this._value()
  }

  flatMap (fn) {
    return this.map(fn).join()
  }
}
const readFileIOMonad = function (filename) {
  return new IOMonad(function () {
    return fs.readFileSync(filename, 'utf-8')
  })
}
const printMonad = function (x) {
  return new IOMonad(function () {
    console.log(x)
    return x
  })
}
const catMonad = readFileIOMonad('package.json')
                  .map(x => fp.toUpper(x))
                  .flatMap(printMonad)
                  .join()
console.log('<----------------catMonad---------------->')
console.log(catMonad)


/* Folktale中的compose,curry */
let ff = curry(2, (x, y) => x + y)
console.log(ff(1, 2))
console.log(compose(fp.toUpper, fp.first)(['one', 'two']))

/* Folktale中的task函子,执行异步任务 */
function readFile (filename) {
  return task(resolver => {
    fs.readFile(filename, 'utf-8', (err, data) => {
      if (err) resolver.reject(err)
      resolver.resolve(data)
    })
  })
}

readFile('package.json')
  .map(fp.split('\n'))
  .map(fp.find(x => x.includes('version')))
  .run()
  .listen({
    onRejected: err => {
      console.log(err)
    },
    onResolved: value => {
      console.log(value)
    }
  })

  /* Point函子是实现来of静态方法的函子,of方法是为了避免使用new来创建对象,更深层的含义是of方法用来把值放到上下文Context(把值放到容器中,使用map来处理值),上文的的函子都是Point函子 */
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值