天天看點

10分鐘入門javascript函數式程式設計

作者:前端餐廳

1. 函數式程式設計

1.什麼是函數式程式設計

函數式程式設計是⼀種程式設計範式,強調使⽤函數來組合和處理資料。将運算過程抽象成成函數,可以複⽤。

常⻅的程式設計範式有:

  • ⾯向過程程式設計(Procedural Programming)PP:按照步驟來實作,将程式分解為過程和函數。這些過程和函數按順序執⾏來完成任務。
  • ⾯向對象程式設計(Object-Oriented Programming)OOP:将程式分解為對象,每個對象都有⾃⼰的狀态和⾏為。⾯向對象的核⼼是(類,執行個體,繼承,封裝,多态)
  • 函數式程式設計(Functional Programming)FP:使⽤函數來組合和處理資料,描述資料之間的映射。函數指的并不是程式設計語⾔中的函數,指的是數學意義上的函數 y=f(x) 輸⼊映射輸出⼀個函數 f 接收⼀個參數 x,并根據 x 計算傳回⼀個結果 y
// ⾯向過程
const arr = [1, 2, 3, 4, 5]
let sum = 0
for (let i = 0; i < arr.length; i++) {
  sum += arr[i]
}
console.log(sum)
// ⾯向對象
class Calc {
  constructor() {
    this.sum = 0
  }
  add(arr) {
    for (let i = 0; i < arr.length; i++) {
      this.sum += arr[i]
    }
  }
}
const calc = new Calc()
calc.add([1, 2, 3, 4, 5])
console.log(calc.sum)
// 函數式程式設計
const sum = arr.reduce((memo, cur) => memo + cur, 0) // ⾼階函數 + 純函數
console.log(sum)
           

2.函數式程式設計的優勢​

  • 可維護性:函數式程式設計的程式通常更加簡潔和可讀,因為它們避免了狀态變化和副作⽤。這使得代碼更易于了解和維護。
  • 可測試性:由于函數式程式設計程式通常是⽆副作⽤的,是以可以很容易地對其進⾏單元測試。
  • 并發性:函數式程式設計程式通常是⽆副作⽤的,是以可以很容易地并⾏地執⾏。
  • 擴充性:函數式程式設計程式通常是純函數,可以很容易地組合和重⽤。
  • 可靠性:函數式程式設計程式通常是⽆副作⽤的,是以可以很容易地預測其⾏為。
  • Vue3 也開始擁抱函數式程式設計,函數式程式設計可以抛棄 this,打包過程中更好的利⽤ tree-shaking 過濾⽆⽤的代碼

3.函數是⼀等公⺠​

First-class Function(頭等函數)當⼀⻔程式設計語⾔的函數可以被當作變量⼀樣⽤時,則稱這⻔語⾔擁有頭等函數。
  • 函數可以存儲在變量中
  • 函數可以作為參數
  • 函數可以作為傳回值

4.⾼階函數(Higher-order function)​

  • ⼀個函數的參數是⼀個函數,或者⼀個函數的傳回值是⼀個函數。則稱這個函數是⾼階函數。

4.1函數作為參數​

// 通過函數的組合,抽象掉運算過程,封裝實作的過程
Array.prototype.reduce = function (callback, startVal) {
  let arr = this
  let acc = typeof startVal === "undefined" ? arr[0] : startVal
  let sIndex = typeof startVal === "undefined" ? 1 : 0
  for (let i = sIndex; i < arr.length; i++) {
    acc = callback(acc, arr[i], i, arr)
  }
  return acc
}
           
// AOP切⽚程式設計,對函數進⾏擴充
Function.prototype.before = function (beforefn) {
  return (...args) => {
    beforefn.call(this, ...args)
    return this(...args)
  }
}
           

4.2函數作為傳回值​

// 緩存邏輯
function exec(a, b) {
  console.log("exec~~~")
  return a + b
}
const memoize = (fn, resolver) => {
  const cache = new Map()
  return (...args) => {
    // 根據resolver計算key
    const key = typeof resolver === "function" ? resolver(...args) : args[0]
    let result = cache.get(key)
    if (result === undefined) {
      result = fn(...args)
      cache.set(key, result)
    }
    return result
  }
}
const resolver = (...args) => JSON.stringify(args)
let memoizedExec = memoize(exec, resolver)
console.log(memoizedExec(1, 2))
console.log(memoizedExec(1, 2))
           
// 利⽤閉包緩存。 當函數可以記住并通路所在的詞法作⽤域,即使函數是目前詞法作⽤域之外執⾏,這時就産⽣了閉包。
function after(count, callback) {
  return () => {
    if (--count === 0) {
      callback()
    }
  }
}
           

5.純函數​

相同的輸⼊永遠會得到相同的輸出,⽽且沒有任何的副作⽤。(不會對外部環境産⽣影響,并且不依賴于外部狀态)

// 純函數
function sum(a, b) {
  return a + b // 相同的輸⼊得到相同的輸出
}
// ⾮純函數
let count = 0
function counter() {
  count++ // 依賴外部狀态,多次調⽤傳回結果不同
  return count
}
let date = new Date()
function getTime() {
  // 不同時間調⽤,傳回值不同
  return date.toLocaleTimeString()
}
           

常⻅副作⽤:

  • 對全局變量或靜态變量的修改
  • 對外部資源的通路(如⽂件、資料庫、⽹絡 http 請求)
  • 對系統狀态的修改 (環境變量)
  • 對共享記憶體的修改
  • DOM 通路,列印/log 等

副作⽤使得⽅法通⽤性降低,讓代碼難以了解和預測,測試困難,導緻靜态問題等。

lodash 庫中所有的⽅法都是純函數

純函數的好處: 可緩存(輸⼊相同輸出相同)、可測試(通過輸⼊輸出⽅便測試)、并⾏處理(可以在多線程環境下并⾏執⾏)

所有在開發時我們會采⽤純函數及統⼀狀态管理

6.柯⾥化​

柯⾥化是⼀種函數轉換技術,它将⼀個多參數函數轉換為⼀系列單參數函數。與之類似的偏函數是指對于⼀個函數,固定其中⼀些參數的值,⽣成⼀個新函數,這個新函數接受剩下的參數

function isType(typing, val) {
  return Object.prototype.toString.call(val) === `[object ${typing}]`
}
// 每次執⾏都需要傳⼊字元串, 可以利⽤⾼階函數來實作參數的保留。 閉包的機制(執⾏上下⽂不會被銷毀)
function isType(typing) {
  // typing
  return function (val) {
    // isString/ isNumber
    return Object.prototype.toString.call(val) === `[object ${typing}]`
  }
}
const util = {}
;["String", "Number", "Boolean"].forEach((typing) => {
  util["is" + typing] = isType(typing)
})
           

lodash 中的柯⾥化函數。被科⾥化的函數所需的參數都被提供則執⾏原函數,否則繼續傳回函數等待接收剩餘的參數

let curried = _.curry(isType) // 将函數進⾏柯⾥化處理
const isString = curried("String") // 緩存參數
           
function add(a, b, c) {
  return a + b + c
}
function curry(func) {
  let curried = (...args) => {
    if (args.length < func.length) {
      return (...rest) => curried(...args, ...rest)
    }
    return func(...args)
  }
  return curried
}
let curriedAdd = curry(add)
console.log(curriedAdd(1, 2, 3))
console.log(curriedAdd(1)(2, 3))
console.log(curriedAdd(1)(2)(3))
           
通過柯⾥化可以實作緩存固定的參數傳回新的函數。讓函數的粒度更⼩。⽣成的⼀元函數更加⽅便組合使⽤

7.函數組合​

早期常⻅的函數組合寫法:洋蔥模型 c(b(a()))、過濾器 a() | b() | c()

函數的組合可以将細粒度的函數重新組合成⼀個新的函數。最終将資料傳⼊組合後的新函數,得到最終的結果。

常⻅的有

  • redux 中的 compose
  • koa、express 中間件實作原理
function double(n) {
  return n * 2
}
function toFixed(n) {
  return n.toFixed(2)
}
function addPrefix(n) {
  return "£" + n
}
const _ = require("lodash")
function flowRight(...fns) {
  if (fns.length === 0) {
    return fns[0]
  }
  return fns.reduceRight((a, b) => {
    return (...args) => b(a(...args))
  })
}
// a => (...args) => toFiexed(double(...args))
// b => addPrefix
// (...args)=> addPrefix(((...args) =>toFiexed(double(...args)))(...args))
const composedFn = flowRight(addPrefix, toFixed, double)
const returnVal = composedFn(10000)
console.log(returnVal)
const _ = require("lodash")
const str = "click button" //CLICK_BUTTON
let flow1 = _.split(str, " ")
let flow2 = _.join(flow1, "_")
let flow3 = _.toUpper(flow2)
console.log(flow3)
// 将函數進⾏組合,先将函數進⾏轉化
const split = _.curry((sep, str) => _.split(str, sep))
const join = _.curry((sep, str) => _.join(str, sep))
const composedFn1 = _.flowRight(_.toUpper, join("_"), split(" "))
console.log(composedFn1(str))
// lodash函數式程式設計,幫我們⾃動科⾥化,并且資料最後傳⼊
const lodash = require("lodash/fp")
const composedFn2 = lodash.flowRight(
  _.toUpper,
  lodash.join("_"),
  lodash.split(" ")
)
console.log(composedFn2(str))
// 這種模式我們也稱之為PointFree,把資料處理的過程先定義成⼀種與參數⽆關的合成運算就叫 Pointfree
           
總結:什麼是函數式程式設計? 函數式程式設計的基礎:純函數、柯⾥化、函數組合。将運算抽象成函數,可以利⽤這些函數進⾏重⽤組合。

8.解決異步并發問題​

8.1哨兵變量​

const fs = require("fs") // file system
const path = require("path")
let times = 0 // 哨兵變量
let school = {}
function out(key, value) {
  school[key] = value
  if (++times == 2) {
    console.log(school)
  }
}
fs.readFile(path.resolve(__dirname, "age.txt"), "utf8", function (err, data) {
  out("age", data)
})
fs.readFile(path.resolve(__dirname, "name.txt"), "utf8", function (err, data) {
  out("name", data)
})
           

8.2函數式程式設計​

const fs = require("fs") // file system
const path = require("path")
function after(times, callback) {
  // ⾼階函數來解決異步并發問題
  let data = {}
  return function (key, value) {
    data[key] = value
    if (--times === 0) {
      callback(data)
    }
  }
}
let out = after(2, (data) => {
  console.log(data)
})
fs.readFile(path.resolve(__dirname, "age.txt"), "utf8", function (err, data) {
  out("age", data)
})
fs.readFile(path.resolve(__dirname, "name.txt"), "utf8", function (err, data) {
  out("name", data)
})
           

8.3釋出訂閱模式​

const fs = require("fs")
const path = require("path")
// 釋出訂閱的核⼼就是将訂閱函數存放到數組中,稍後事情發⽣了 循環數組依次調⽤
// 不訂閱也能釋出 (訂閱和釋出之間沒有任何關系)
let school = {}
let events = {
  _arr: [],
  on(callback) {
    // 将要訂閱的函數儲存起來
    this._arr.push(callback)
  },
  emit(key, value) {
    school[key] = value
    this._arr.forEach((callback) => callback(school))
  },
}
events.on((data) => {
  if (Object.keys(data).length === 2) {
    console.log(data)
  }
})
events.on((data) => {
  console.log("讀取⼀個完畢", data)
})
fs.readFile(path.resolve(__dirname, "age.txt"), "utf8", function (err, data) {
  events.emit("age", data)
})
fs.readFile(path.resolve(__dirname, "name.txt"), "utf8", function (err, data) {
  events.emit("name", data)
})
// 釋出訂閱模式,可以監控到每次完成的情況,⽽且可以⾃⼰控制邏輯           

繼續閱讀