JavaScript函數(shù)式編程實現(xiàn)介紹
為什么要學習函數(shù)式編程
Vue進入3.*(One Piece 海賊王)世代后,引入的setup語法,頗有向老大哥React看齊的意思,說不定前端以后還真是一個框架的天下。話歸正傳,框架的趨勢確實是對開發(fā)者的js功底要求更為嚴格了,無論是hooks、setup,都離不開函數(shù)式編程,抽離代碼可復用邏輯,更好地組織及復用代碼,有一點我感到很高興的是,終于可以拋棄煩人的this了,當然,這也不是我為偷懶而生出這樣的感想,人家道格拉斯老爺子可是在他的新書《JavaScript悟道》里極力吐槽了一下this,所以,也算是像js大佬看齊了。所以,要想不被前端日新月異的新技術給沖昏頭腦,還是適時回來重學一下JavaScript吧。
什么是函數(shù)式編程
函數(shù)式編程(Functional Programming, FP),F(xiàn)P 是編程范式之一,我們常聽說的編程范式還有面向過程編程、面向對象編程。
面向對象編程:面向對象有三大特性,通過封裝、繼承和多態(tài)來演示事物之間的聯(lián)系,如果更寬泛來說,抽象也應該算進去,但是由于面向對象的本質就是抽象,其不算是三大特性也不為過。
函數(shù)式編程:函數(shù)式編程的思想主要就是對運算過程進行抽象,它更像一個黑盒,你給入特定的輸出,進過黑盒運算后再返回運算結果。你可以將其理解為數(shù)學中的y = f(x)。
- 程序的本質:根據(jù)輸入進行某種運算得到相應的輸出。
- x -> f(聯(lián)系、映射) -> y, y = f(x)
- 函數(shù)式編程中的函數(shù)其實對應數(shù)學中的函數(shù),即映射關系。
- 相同的輸入始終要得到相同的輸出(純函數(shù))
- 可復用
前置知識
函數(shù)是一等公民
作為一名有一定經驗的前端開發(fā)者,你一定對JavaScript中“函數(shù)是一等公民”這一說法不陌生。
這里給出權威文檔MDN的定義:當一門編程語言的函數(shù)可以被當作變量一樣用時,則稱這門語言擁有頭等函數(shù)。例如,在這門語言中,函數(shù)可以被當作參數(shù)傳遞給其他函數(shù),可以作為另一個函數(shù)的返回值,還可以被賦值給一個變量。
函數(shù)可以儲存在變量中
let fn = function() {
console.log('Hello First-class Function')
}
fn()
函數(shù)作為參數(shù)
function foo(arr, fun) {
for (let i = 0; i < arr.length; i++) {
fun(arr[i])
}
}
const array = [1, 2, 3, 4]
foo(array, function(a) { console.log(a) })函數(shù)作為返回值
function fun() {
return function () {
consoel.log('哈哈哈')
}
}
const fn = fun()
fn()高階函數(shù)
什么是高階函數(shù)
高階函數(shù)
- 可以把函數(shù)作為參數(shù)傳遞給另外一個函數(shù)
- 可以把函數(shù)作為另外一個函數(shù)的返回結果
函數(shù)作為參數(shù)(為了避免文章篇幅過長,后面的演示代碼就不給出測試代碼了,讀者可自行復制文章代碼在本地編輯器上調試)
function filter(array, fn) {
let results = []
for (let i = 0; i < array.length; i++) {
if (fn(array[i])) {
results.push(array[i])
}
}
return results
}
// 測試
let arr = [1, 3, 4, 7, 8]
const results = filter(arr, function(num) {
return num > 7
})
console.log(results) // [8]函數(shù)作為返回值
// 考慮一個場景,在網絡延遲情況下,用戶點擊支付,你一定不想要用戶點完支付沒反應后點擊下一次支付再重新支付一次,不然,你的公司就離倒閉不遠了。
// 所以考慮一下once函數(shù)
function once(fn) {
let done = false
return function() {
if (!done) {
done = true
return fn.apply(this, arguments)
}
}
}
let pay = once(function (money) {
console.log(`支付: ${money} RMB`)
})
pay(5)
pay(5)
pay(5)
pay(5)
// 5使用高階函數(shù)的意義
- 抽象可以幫我們屏蔽細節(jié),只需要關注目標
- 高階函數(shù)是用來抽象通用的問題
常用高階函數(shù)
- forEach(已實現(xiàn))
- map
const map = (array, fn) => {
let results = []
for (let value of array) {
results.push(fn(value))
}
return results
}- filter
- every
const every = (array, fn) => {
let result = true
for (let value of array) {
result = fn(value)
if (!result) {
break
}
}
return result
}
- some
const some = (array, fn) => {
let result = false
for (let value of array) {
result = fn(value)
if (result) {
break
}
}
return result
}
- find/findIndex
- reduce
- sort
閉包
閉包 (Closure):函數(shù)和其周圍的狀態(tài)(詞法環(huán)境)的引用捆綁在一起形成閉包。
閉包的本質:函數(shù)在執(zhí)行的時候會放到一個執(zhí)行棧上當函數(shù)執(zhí)行完畢之后會從執(zhí)行棧上移除,但是堆上的作用域成員因為被外部引用不能釋放,因此內部函數(shù)依然可以訪問外部函數(shù)的成員。
function makePower(power) {
return function (num) {
return Math.pow(num, power)
}
}
// 求平方及立方
let power2 = makePower(2)
let power3 = makePower(3)
console.log(power2(4)) // 16
console.log(power2(5)) // 25
console.log(power3(4)) // 64function maekSalary(base) {
return function (performance) {
return base + performance
}
}
let salaryLevel1 = makeSalary(12000)
let salaryLevel2 = makeSalary(15000)
console.log(salaryLevel1(2000)) // 14000
console.log(salaryLevel2(3000)) // 18000其實上面這兩個函數(shù)都是差不多的,都是通過維持對原函數(shù)內部成員的引用。具體可以通過瀏覽器調試工具自行了解。
純函數(shù)
純函數(shù)概念
純函數(shù):相同的輸入永遠會得到相同的輸出
lodash 是一個純函數(shù)的功能庫,提供了對數(shù)組、數(shù)字、對象、字符串、函數(shù)等操作的一些方法。有人可能會有這樣的疑惑,隨著ECMAScript的演進,lodash中很多方法都已經在ES6+中逐步實現(xiàn)了,那么學習其還有必要嗎?其實不然,lodash中還是有很多很好用的工具函數(shù)的,比如說,防抖節(jié)流是前端工作中經常用到的,你可不想每次都手寫一個函數(shù)吧?更何況沒有一點js功底還寫不出來呢。
話歸正傳,來看看數(shù)組的兩個方法:slice和splice。
- slice 返回數(shù)組中的指定部分,不會改變原數(shù)組
- splice 對數(shù)組進行操作返回該數(shù)組,會改變原數(shù)組
let array = [1, 2, 3, 4, 5] // 純函數(shù) console.log(array.slice(0, 3)) console.log(array.slice(0, 3)) console.log(array.slice(0, 3)) // 不純的函數(shù) console.log(array.splice(0, 3)) console.log(array.splice(0, 3)) console.log(array.splice(0, 3))
純函數(shù)的好處
可緩存
因為純函數(shù)對相同的輸入始終有相同的結果,所以可以把純函數(shù)的結果緩存起來
function getArea(r) {
console.log(r)
return Math.PI * r * r
}
function memoize(f) {
let cache = {}
return function() {
let key = JSON.stringify(arguments)
cache[key] = cache[key] || f.apply(f, arguments)
return cache[key]
}
}
let getAreaWithMemory = memoize(getArea)
console.log(getAreaWithMemory(4))
console.log(getAreaWithMemory(4))
console.log(getAreaWithMemory(4))
// 4
// 50.26548245743669
// 50.26548245743669
// 50.26548245743669
可測試
純函數(shù)讓測試更方便
并行處理
在多線程環(huán)境下并行操作共享的內存數(shù)據(jù)很可能會出現(xiàn)意外情況
純函數(shù)不需要訪問共享的內存數(shù)據(jù),所以在并行環(huán)境下可以任意運行純函數(shù) (Web Worker)
副作用
// 不純的
let mini = 18
function checkAge (age) {
return age >= mini
}
// 純的(有硬編碼,后續(xù)可以通過柯里化解決)
function checkAge (age) {
let mini = 18
return age >= mini
}副作用讓一個函數(shù)變的不純(如上例),純函數(shù)的根據(jù)相同的輸入返回相同的輸出,如果函數(shù)依賴于外部的狀態(tài)就無法保證輸出相同,就會帶來副作用。
柯里化
柯里化的概念:當一個函數(shù)有多個參數(shù)的時候先傳遞一部分參數(shù)調用它(這部分參數(shù)以后永遠不變),然后返回一個新的函數(shù)接收剩余的參數(shù),返回結果。
柯里化就可以解決上面代碼中的硬編碼問題
// 普通的純函數(shù)
function checkAge(min, age) {
return age >= min
}
// 函數(shù)的柯里化
function checkAge(min) {
return function(age) {
return age >= min
}
}
// 當然,上面的代碼也可以用ES6中的箭頭函數(shù)來改造
const checkAge = (min) => (age => age >= min)下面來手寫一個curry函數(shù)
function curry(func) {
return function curriedFn(...args) {
if (args.length < func.length) {
return function() {
return curriedFn(...args.concat(Array.from(arguments)))
}
}
return func(...args)
}
}
函數(shù)組合
看了這么多代碼,你肯定會覺得函數(shù)里面有很多return看起來不是很好看,事實也確是如此,所以這就要引出函數(shù)組合這個概念。
純函數(shù)和柯里化很容易寫出洋蔥代碼 h(g(f(x)))
獲取數(shù)組的最后一個元素再轉換成大寫字母, .toUpper(.first(_.reverse(array))) (這些都是lodash中的方法)
函數(shù)組合可以讓我們把細粒度的函數(shù)重新組合生成一個新的函數(shù)
你可以把其想象成一根管道,你將fn管道拆分成fn1、fn2、fn3三個管道,即將不同處理邏輯封裝在不同的函數(shù)中,然后通過一個compose函數(shù)進行整合,將其變?yōu)橐粋€函數(shù)。
fn = compose(f1, f2, f3) b = fn(a)
Functor(函子)
什么是Functor
- 容器:包含值和值的變形關系(這個變形關系就是函數(shù))
- 函子:是一個特殊的容器,通過一個普通的對象來實現(xiàn),該對象具有 map 方法,map 行一個函數(shù)對值進行處理(變形關系)
// Functor 函子 一個容器,包裹一個值
class Container {
constructor(value) {
this._value = value
}
// map 方法,傳入變形關系,將容器里的每一個值映射到另一個容器
map(fn) {
return new Container(fn(this._value))
}
}
let r = new Container(5)
.map(x => x + 1)
.map(x => x * x)
console.log(r) // 36總結
- 函數(shù)式編程的運算不直接操作值,而是由函子完成
- 函子就是一個實現(xiàn)了 map 契約的對象
- 我們可以把函子想象成一個盒子,這個盒子里封裝了一個值
- 想要處理盒子中的值,我們需要給盒子的 map 方法傳遞一個處理值的函數(shù)(純函數(shù)),由這個函數(shù)來對值進行處理
- 最終 map 方法返回一個包含新值的盒子(函子)
可能你不習慣在代碼中看到new關鍵字,所以可以在容器中實現(xiàn)一個of方法。
class Container {
static of (value) {
return new Container(value)
}
constructor(value) {
this._value = value
}
map(fn) {
return Container.of(fn(this._value))
}
}MayBe 函子
上面的代碼中如果傳入一個null 或 undefined的話,代碼就會拋出錯誤,所以需要再實現(xiàn)一個方法
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() {
return this._value == null // 此處雙等號等價于this._value === null || this._value === undefined
}
}你看下上面的代碼,是不是健壯性就好一點了呢?
Either函子
在MayBe函子中,很難確認哪一步產生的空值問題。所以就有了Either
class Left {
static of(value) {
return new Left(value)
}
constructor(value) {
this._value = value
}
map(fn) {
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 })
}
}到此這篇關于JavaScript函數(shù)式編程實現(xiàn)介紹的文章就介紹到這了,更多相關JS函數(shù)式編程內容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
相關文章
Javascript動態(tài)創(chuàng)建表格及刪除行列的方法
這篇文章主要介紹了Javascript動態(tài)創(chuàng)建表格及刪除行列的方法,涉及javascript動態(tài)操作表格的相關技巧,需要的朋友可以參考下2015-05-05
讓html元素隨瀏覽器的大小自適應垂直居中的實現(xiàn)方法
下面小編就為大家?guī)硪黄宧tml元素隨瀏覽器的大小自適應垂直居中的實現(xiàn)方法。小編覺得挺不錯的,現(xiàn)在就分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2016-10-10
詳解JavaScript如何實現(xiàn)一個簡易的Promise對象
Promise對象的作用將異步操作以同步操作的流程表達出來,避免層層嵌套的回調函數(shù),而且Promise提供了統(tǒng)一的接口,使得控制異步操作更加容易。本文介紹了如何實現(xiàn)一個簡單的Promise對象,需要的可以參考一下2022-11-11
electron-builder 的基本使用及electron打包步驟
electron-builder 作為一個用于 Electron 應用程序打包的工具,需要下載并使用 Electron 運行時來創(chuàng)建可執(zhí)行文件,這篇文章主要介紹了electron-builder 的基本使用,需要的朋友可以參考下2023-12-12
IE6-IE9使用JSON、table.innerHTML所引發(fā)的問題
這篇文章主要介紹了IE6-IE9使用JSON、table.innerHTML所引發(fā)的問題 ,需要的朋友可以參考下2015-12-12

