函數式編程—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食用更加有毒

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