函数式编程—2—纯函数(纯函数优势、副作用、loadsh、简单的jest进行单元测试)


概念

  • 相拥的属于永远会得到相同的输出,而且没有任何可观察的副作用。
  • 函数式编程不会保留计算中间的结果,所以边开那个是不可改变的(无状态的)
  • 可以吧一个函数的执行结果交给另外一个函数去处理

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

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

返回数组部分的指定部分,不会改变原数组,并且输入相同的值得到的总是相同的

const arr1 = [1, 2, 3]
console.log(arr1.slice(0,1)) // 1
console.log(arr1.slice(0,1)) // 1
console.log(arr1) // [ 1, 2, 3 ]
splice

对数组进行操作返回该数组,会改变原数组,输入相同的值得到的总是不相同的

const arr2 = [1, 2, 3]
console.log(arr1.splice(0,1)) // 1
console.log(arr1.splice(0,1)) // 2
console.log(arr1) // [ 3 ]
实现一个自己的纯函数 sum
function sum(n,m){
    return n+m
}
sum(1+2) // 3
sum(1+2) // 3

loadsh

是一个一致性、模块化、高性能的 JavaScript 实用工具库
loadsh传送门

简单实用

首先需要下载loadsh

yarn && yarn add loadsh

开始使用

const _ = require('loadsh')

const arr = ['xiaoMing','liLei','xiaoHong']

// 返回传入数组的第一个元素
console.log(_.first(arr)) // xiaoMing
// 返回传入数组的第一个元素
console.log(_.head(arr)) // xiaoMing
// 返回传入数组的最后一个元素
console.log(_.last(arr)) // xiaoHong

// 字符串转大写
console.log(_.toUpper(_.first(arr))) // XIAOMING

// 数组反转
console.log(_.reverse(arr)) // [ 'xiaoHong', 'liLei', 'xiaoMing' ]

// each
_.each(arr,(val,index)=>{
    console.log(val,index)
    // xiaoHong 0
    // liLei 1
    // xiaoMing 2
})

console.log(_.includes(arr,'liLei')) // true
console.log(_.find(arr,(val)=>val==='liLei')) // liLei
console.log(_.findIndex(arr,(val)=>val==='liLei')) // 1

其实loadsh就是在es6之前封装的函数库。虽然现在es6及更高的版本已经出现,但是loadsh还是有学习的必要的,例如后面的函数柯里化、函数组合等等。

纯函数优势

可缓存

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

const _ = require('loadsh')
function getAtea(r){
    console.log('getAtea函数执行')
    return Math.PI*r*r
}
console.log(getAtea(2)) 
// getAtea函数执行  
// 12.566370614359172

const memoizeGetAtea = _.memoize(getAtea)
console.log(memoizeGetAtea(2))
// getAtea函数执行
// 12.566370614359172
console.log(memoizeGetAtea(2))
// 此次执行memoizeGetAtea但是并没有调用getAtea 而是执行上一次的计算结果
// 12.566370614359172

console.log(memoizeGetAtea(3))
// getAtea函数执行   因为 半径为3圆的面积并没有被缓存 所以会执行getAtea
// 28.274333882308138
console.log(memoizeGetAtea(3))
// 此次已经缓存了所以不会调用getAtea
// 28.274333882308138

模拟memoize

function memoize(fn){
    let cache = {}
    return function(){
        let key = JSON.stringify(arguments)
        cache[key] = cache[key] || fn.apply(undefined,arguments)
        return cache[key]
    }
}
function getAtea(r){
    console.log('getAtea函数执行')
    return Math.PI*r*r
}
const memoizeGetAtea = memoize(getAtea)
console.log(memoizeGetAtea(2))
// getAtea函数执行
// 12.566370614359172
console.log(memoizeGetAtea(2))
// 12.566370614359172

可测试(单元测试)

跑个题 实现一个最简单单元测试及运行环境。

# 下载所需依赖
yarn add jest babel-jest babel-core babel-preset-env regenerator-runtime --dev  

新建 .babelrc

{
    "presets": [
        "env"
    ]
}

修改package.json

{
  "scripts": {
    "test": "jest"
  },
  "dependencies": {
    "loadsh": "^0.0.4"
  },
  "devDependencies": {
    "babel-core": "^6.26.3",
    "babel-jest": "^26.0.1",
    "babel-preset-env": "^1.7.0",
    "jest": "^26.0.1",
    "regenerator-runtime": "^0.13.5"
  }
}

目录结构如下

├── package-lock.json
├── package.json
├── src
│   └── index.js
├── test
│   └── index.test.js
└── yarn.lock

src/index.js 导出模块

function add (n,m){
    return n+m
}
function reduce (n,m){
    return n-m
}
export {
    add,
    reduce
}

test/index.test.js

import {add,reduce} from '../src/index';

test('add(1 + 1) 等于 2', () => {
    expect(add(1, 1)).toBe(2);
})

test('reduce(2 - 1) 等于 1', () => {
    expect(reduce(2, 1)).toBe(1);
})

test('add(1 + 1) 等于 2', () => {
    expect(add('1', 1)).toBe(2);
})

yarn test

并行处理

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

副作用

副作用让一个函数变得不纯,如果函数依赖于外部的状态就无法保证相同的输出,就会带来副作用

// 不纯函数
const mini = 18
function checkAge(age){
    return age > mini
}

// 纯函数(存在硬编码,后续可以通过柯里化解决)
function checkAge2(){
    const mini = 18
    return age >= mini
}

副作用主要来源

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

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

ps:函数式编程+TypeScript食用更加有毒

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章