为什么要学习函数

函数式变成是非常古老的概念,早于第一台计算机的诞生,函数式编程历史

那我们为什么现在还需要学习函数式编程?

  • 函数式编程是随着 React 的流行受到越来越多的关注
  • Vue3 也开始拥抱函数式变成
  • 函数式编程可以抛弃 this
  • 打包的过程中可以更好的利用 tree shaking 过滤无用代码
  • 方便测试、方便并行处理
  • 有很多库可以帮助我们进行函数式开发:lodash、underscore、ramda

函数式编程概念

函数式编程(Functional Programming, FP)FP是编程范式之一,我们常说的编程范式还有面向对象,面向过程编程

  • 面向对象编程的思维方式:把现实世界中的事物抽象成程序世界中的类和对象,通过封装、继承和多态来演示事物事件的关系
  • 函数式编程的思维方式:把现实世界的事物和事物之间的联系抽象到程序世界(对运算的过程进行抽象)

    • 程序世界的本质:根据输入通过某种运算获得相应的输出,程序开发过程中会涉及很多有输入输出的函数
    • x-> f(联系、映射) -> y, y=f(x)
    • 函数式编程中的函数不是程序中的函数(方法),而是数学中的函数即映射关系,例如:y=sin(x), x和y的关系
    • 相同的输入始终得到相同的输出
    • 函数式编程用来描述数据(函数)之间的映射
// 非函数式
let num1 = 2
let num2 = 3
let sum = num1 + num2
console.log(sum)

// 函数式
function add (n1, n2) {
  return n1 + n2
}
let sum = add(1, 2)
console.log(sum)

函数式一等公民

First-class Function

  • 函数可以存储在变量中
  • 函数可以作为参数
  • 函数作为返回值

在 JavaScript 中 函数就是一个普通对象(可以通过 new Function()),我们可以把函数存储到变量/数组中,他还可以作为另一个函数的参数和返回值,甚至我们可以在程序运行的时候通过 new Function('alert(1)') 来构造一个新的函数

把函数赋值给变量

// 把函数赋值给变量
let fn = function () {
  console.log("hello first-class function")
}

// 实例
const BlogController = {
  index (posts) {return Views.index(posts)},
  create (posts) {return Views.create(posts)},
  delete (posts) {return Views.delete(posts)},
}

// 优化
const BlogController = {
  index : Views.index, 
  create : Views.create,
  delete : Views.delete
}

函数式一等公民式要学习高阶函数,函数科里化基础

高阶函数

什么是高阶函数

  • 高阶函数(Higher-order function)

    • 可以把函数作为一个参数传递给另一个函数
    • 可以把函数作为另一个函数的返回值

函数作为参数

模拟实现一个 forEach 函数来遍历数组,我们设定第一个参数为数组,就是我们需要遍历的数组,因为我们需要遍历数组所以需要写一个循环、第二个参数为处理数组每一项的函数,因为需要处理数组的每一项,所以我们在循环中调用传递进来的那个函数并把每一项的值传递给处理函数

// 高阶函数-实现forEach 函数
function forEach (array, fn) {
  for (let i = 0; i < array.length; i++) {
    fn(array[i])
  }
}

// 测试
let arr = [1,2,3,4,5,6,7,8]
forEach(arr, function (item) {
  console.log(item)
})

模拟实现一个 filter 函数,他同样需要两个函数,第一个是需要过滤的函数,第二个是处理满足条件的函数

// 高阶函数-实现 filter 函数
function filter (array, fn) {
  let results = [];
  for (let i = 0; i < array.length; i++) {
    if (fn(array[i])) {
        results.push(array[i])
    }
  }
}

// 测试
let arr = [1,2,3,4,5,6,7,8]
let r = filter(arr, function (item) {
  return    item % 2 === 0
})
console.log(r)

函数作为返回值

函数作为返回值的基本使用

function makeFn () {
  let msg = 'Hello function'
  return    function () {
    console.log(msg)
  }
}
// 调用1
const fn = makeFn()
fn()
// 调用方式2
makeF()()

实现 once 函数(对于一个函数只执行一次)例如支付的时候一个订单不管用户点击好多次只会生成一次,所以我们可以用 once 函数来实现。once 是让函数只执行一次,所以需要接受一个函数,在once中定义一个变量来控制这个函数是否执行过了,在返回的函数中我们判断是否执行过如果没有执行的话我们就执行 真正需要执行的函数

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

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

pay(1)
pay(1)
pay(1)
pay(1)

使用高阶函数的意义

高阶函数帮我们屏蔽细节,抽象通用的东西。在相同的逻辑的时候只需要调用高阶函数就行了。我们之前实现的 forEach 和 filter 就是抽象通用逻辑的高阶函数

  • 抽象可以帮我们屏蔽细节,只需要关注与我们的目标
  • 高阶函数是用来抽象通用的问题

常用的高阶函数

  • forEach
  • map
  • filter
  • every
  • some
  • find/findIndex
  • reduce
  • sort
  • .....

实现map方法

map() 方法创建一个新数组,其结果是该数组中的每个元素是调用一次提供的函数后的返回值。

map 方法第一个参数是需要处理的数组,第二个参数是处理的方法,在map函数中需要定义一个数组,在循环的时候需要把每一项处理后的结果储存到定义的数组中,循环完之后返回这个数组

const map = (array, fn) => {
  let results = []
  for (let i = 0; i < array.length; i++){
    results.push(fn(array[i]))
  }
  return    results
}

实现every 函数

every() 方法测试一个数组内的所有元素是否都能通过某个指定函数的测试。它返回一个布尔值。

我们默认条件为真,在循环中赋值条件为处理函数后的结果,如果有一项不满足就跳出循环,并返回条件

const every = (array, fn) => {
  let result = true;
  for (let i = 0; i < array.length; i++){
    results = fn(array[i])
    if (!result) {
      break;
    }
  }
  return result
}

实现 some 函数

some() 方法测试数组中是不是至少有1个元素通过了被提供的函数测试。它返回的是一个Boolean类型的值。

与 every 函数相反,我们设置默认条件为 false 如果有一个元素满足 则跳出循环 并返回条件

const some = (array, fn) => {
  let result = false;
  for (let i = 0; i < array.length; i++){
    results = fn(array[i])
    if (result) {
      break;
    }
  }
  return result
}

闭包

闭包的概念

闭包(Closure):函数和其周围的状态(词法环境)的引用捆绑在一起形成闭包

可以在另一个作用中调用另一个函数的内部函数并访问到该函数的作用域中的成员

一个函数和对其周围状态(lexical environment,词法环境)的引用捆绑在一起(或者说函数被引用包围),这样的组合就是闭包closure)。也就是说,闭包让你可以在一个内层函数中访问到其外层函数的作用域。在 JavaScript 中,每当创建一个函数,闭包就会在函数创建的同时被创建出来。

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

闭包的案列

实现求n次方的函数

function makePower (power) {
  return function (number) {
    return    Math.pow(number, power)
  }
}

// 测试
let power2 = makePower(2)
let power3 = makePower(3)

console.log(power2(4))
console.log(power2(5))
console.log(power3(4))
console.log(power3(5))

计算工资函数,基本工资 + 绩效工资,同一个级别的员工基本工资相同,但绩效工资不同,所以我们设计一个函数处理相同工资的工资计算

getSalary(12000)
getSalary(12000)
getSalary(12000)
// 
function makeSalary (base) {
  return (performnce) => {
    return base + performnce
  }
}
let salaryLevel1 = makeSalary(10000)
let salaryLevel2 = makeSalary(12000)
let salaryLevel3 = makeSalary(13000)

console.log(salaryLevel1(300))
console.log(salaryLevel1(400))
console.log(salaryLevel1(500))

纯函数

纯函数概念

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

    • 纯函数就类似数学中的函数(用来描述输入和输出之间的关系),y= f(x)

image-20211202223900635

前半部分这个集合是函数的输入

后半部分这个集合是函数的输出

f 这个函数就是描述输入和输出之间的关系

  • 数组的 slicesplice分别是纯函数和不纯的函数

    • slice 截取数组的指定部分,不会改变原数组
    • splice 对数组做操作并且返回该函数,会改变原数组
let array = [1,2,3,4,5,6]

console.log(array.slice(0, 3))
// [1,2,3]
console.log(array.slice(0, 3))
// [1,2,3]
console.log(array.slice(0, 3))
// [1,2,3]


let array2 = [1,2,3,4,5,6]

console.log(array2.splice(0, 3))
// [1,2,3]
console.log(array2.splice(0, 3))
// [4,5,6]
console.log(array2.splice(0, 3))
// []

slice 多次调用相同的输入始终得到相同的输出,所以是纯函数,splice 多次调用之后相同的输入得到的输出都不一致所以不是纯函数,根据相同的输入始终得到相同的输出我们来写一个纯函数

// 实现一个求和的纯函数
function getSum (n1, n2) {
  return n1 + n2
}

console.log(getSum(1, 2)) // 3
console.log(getSum(1, 2)) // 3
console.log(getSum(1, 2)) // 3

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

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

lodash

Lodash 是一个一致性、模块化、高性能的 JavaScript 实用工具库。官网文档地址

安装

npm install lodash

基础方法演示

const _ = require('lodash')

let array = [1,2,3,4,5,6,7,8]

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

console.log(_.toUpper(array))

// 会改变原属组
console.log(_.reverse(array))

// 都有输入和返回值
const r  = _.each(array, (item, index) => {
  console.log(item, index)
})

console.log(r)
// es6 之前没有这个方法 之前使用只好用lodash 这些库或者自己实现功能
_.includes(array, item => {
  return item % 2 === 0
})
_.findIndex(array, item => {
  return item === 5
})

纯函数的好处

可缓存

因为纯函数相同的输入始终得到相同的输出,所以我们可以吧相同的函数的结果缓存起来,

使用 memoize 方法来讲述可缓存

memoize方法创建一个会缓存 func 结果的函数。 如果提供了 resolver ,就用 resolver 的返回值作为 key 缓存函数的结果。 默认情况下用第一个参数作为缓存的 key。 func 在调用时 this 会绑定在缓存函数上。

注意: 缓存会暴露在缓存函数的 cache 上。 它是可以定制的,只要替换了 _.memoize.Cache 构造函数,或实现了Mapdelete, get, has, 和 set方法。

const _ = require('lodash')

function getArea (r) {
  console.log(r)
  return Math.PI * r * r
}
// 会缓存 getArea 的输入输出,第二次相同的输入直接读取缓存里面的值 而不是函数计算的值a
const getAreaWithMemoize = _. (getArea)

console.log(getAreaWithMemoize(200))
console.log(getAreaWithMemoize(200))
console.log(getAreaWithMemoize(200))
console.log(getAreaWithMemoize(200))

自己实现一个 memoize 方法,根据使用lodash中的memoize方法可以看出需要传递一个函数的参数并且返回一个函数,因为把结果缓存起来了所以我们需要一个对象来缓存结果

// 模拟 memoize
// 根据使用得出需要传递一个函数
function memoize(fn) {
  // 需要有个对象缓存值
  const cache = {}
  // 需要抛出一个函数
  return function() {
    // 传递的参数作为key
    let key = JSON.stringify(arguments);
    // 先获取缓存里面的值,如果缓存里面没有的话则执行函数得到结果并缓存到对象
    cache[key] = cache[key] || fn.apply(fn, arguments);

    return cache[key]
  }
}
const getAreaWithMemoize = memoize(getArea)

console.log(getAreaWithMemoize(200))
console.log(getAreaWithMemoize(200))
console.log(getAreaWithMemoize(200))
console.log(getAreaWithMemoize(200))

可测试

为什么说纯函数可测试,因为纯函数有输入和输出、并且相同的输入始终得到相同的输出,而单元测试就是不断的在断言函数的处理结果,所以所有的纯函数都是可以测试的函数

并行处理

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

注: es6 以后已经可以使用 web Worker 开启多个线程了,虽然我们平时大多都是使用单线程

副作用

副作用让一个函数变得不纯。比如下面的代码,如果依赖全局变量的话就无法保证相同的输入还能得到相同的输出了,这就是副作用

// 不纯的
let age = 18

function checkAge (mini) {
  return age >= mini
}

// 纯的
function checkAge (mini) {
  let age = 18
  return age >= mini
}

副作用的来源

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

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