JavaScript函數(shù)式編程實現(xiàn)介紹
為什么要學(xué)習(xí)函數(shù)式編程
Vue進入3.*(One Piece 海賊王)世代后,引入的setup語法,頗有向老大哥React看齊的意思,說不定前端以后還真是一個框架的天下。話歸正傳,框架的趨勢確實是對開發(fā)者的js功底要求更為嚴格了,無論是hooks、setup,都離不開函數(shù)式編程,抽離代碼可復(fù)用邏輯,更好地組織及復(fù)用代碼,有一點我感到很高興的是,終于可以拋棄煩人的this了,當(dāng)然,這也不是我為偷懶而生出這樣的感想,人家道格拉斯老爺子可是在他的新書《JavaScript悟道》里極力吐槽了一下this,所以,也算是像js大佬看齊了。所以,要想不被前端日新月異的新技術(shù)給沖昏頭腦,還是適時回來重學(xué)一下JavaScript吧。
什么是函數(shù)式編程
函數(shù)式編程(Functional Programming, FP),F(xiàn)P 是編程范式之一,我們常聽說的編程范式還有面向過程編程、面向?qū)ο缶幊獭?/p>
面向?qū)ο缶幊蹋好嫦驅(qū)ο笥腥筇匦裕ㄟ^封裝、繼承和多態(tài)來演示事物之間的聯(lián)系,如果更寬泛來說,抽象也應(yīng)該算進去,但是由于面向?qū)ο蟮谋举|(zhì)就是抽象,其不算是三大特性也不為過。
函數(shù)式編程:函數(shù)式編程的思想主要就是對運算過程進行抽象,它更像一個黑盒,你給入特定的輸出,進過黑盒運算后再返回運算結(jié)果。你可以將其理解為數(shù)學(xué)中的y = f(x)。
- 程序的本質(zhì):根據(jù)輸入進行某種運算得到相應(yīng)的輸出。
- x -> f(聯(lián)系、映射) -> y, y = f(x)
- 函數(shù)式編程中的函數(shù)其實對應(yīng)數(shù)學(xué)中的函數(shù),即映射關(guān)系。
- 相同的輸入始終要得到相同的輸出(純函數(shù))
- 可復(fù)用
前置知識
函數(shù)是一等公民
作為一名有一定經(jīng)驗的前端開發(fā)者,你一定對JavaScript中“函數(shù)是一等公民”這一說法不陌生。
這里給出權(quán)威文檔MDN的定義:當(dāng)一門編程語言的函數(shù)可以被當(dāng)作變量一樣用時,則稱這門語言擁有頭等函數(shù)。例如,在這門語言中,函數(shù)可以被當(dāng)作參數(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ù)的返回結(jié)果
函數(shù)作為參數(shù)(為了避免文章篇幅過長,后面的演示代碼就不給出測試代碼了,讀者可自行復(fù)制文章代碼在本地編輯器上調(diào)試)
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ù)作為返回值
// 考慮一個場景,在網(wǎng)絡(luò)延遲情況下,用戶點擊支付,你一定不想要用戶點完支付沒反應(yīng)后點擊下一次支付再重新支付一次,不然,你的公司就離倒閉不遠了。 // 所以考慮一下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é),只需要關(guān)注目標
- 高階函數(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)境)的引用捆綁在一起形成閉包。
閉包的本質(zhì):函數(shù)在執(zhí)行的時候會放到一個執(zhí)行棧上當(dāng)函數(shù)執(zhí)行完畢之后會從執(zhí)行棧上移除,但是堆上的作用域成員因為被外部引用不能釋放,因此內(nèi)部函數(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)) // 64
function 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ù)內(nèi)部成員的引用。具體可以通過瀏覽器調(diào)試工具自行了解。
純函數(shù)
純函數(shù)概念
純函數(shù):相同的輸入永遠會得到相同的輸出
lodash 是一個純函數(shù)的功能庫,提供了對數(shù)組、數(shù)字、對象、字符串、函數(shù)等操作的一些方法。有人可能會有這樣的疑惑,隨著ECMAScript的演進,lodash中很多方法都已經(jīng)在ES6+中逐步實現(xiàn)了,那么學(xué)習(xí)其還有必要嗎?其實不然,lodash中還是有很多很好用的工具函數(shù)的,比如說,防抖節(jié)流是前端工作中經(jīng)常用到的,你可不想每次都手寫一個函數(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ù)對相同的輸入始終有相同的結(jié)果,所以可以把純函數(shù)的結(jié)果緩存起來
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)境下并行操作共享的內(nèi)存數(shù)據(jù)很可能會出現(xiàn)意外情況
純函數(shù)不需要訪問共享的內(nèi)存數(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)就無法保證輸出相同,就會帶來副作用。
柯里化
柯里化的概念:當(dāng)一個函數(shù)有多個參數(shù)的時候先傳遞一部分參數(shù)調(diào)用它(這部分參數(shù)以后永遠不變),然后返回一個新的函數(shù)接收剩余的參數(shù),返回結(jié)果。
柯里化就可以解決上面代碼中的硬編碼問題
// 普通的純函數(shù) function checkAge(min, age) { return age >= min } // 函數(shù)的柯里化 function checkAge(min) { return function(age) { return age >= min } } // 當(dāng)然,上面的代碼也可以用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ù)組的最后一個元素再轉(zhuǎn)換成大寫字母, .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
- 容器:包含值和值的變形關(guān)系(這個變形關(guān)系就是函數(shù))
- 函子:是一個特殊的容器,通過一個普通的對象來實現(xiàn),該對象具有 map 方法,map 行一個函數(shù)對值進行處理(變形關(guān)系)
// Functor 函子 一個容器,包裹一個值 class Container { constructor(value) { this._value = value } // map 方法,傳入變形關(guān)系,將容器里的每一個值映射到另一個容器 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
總結(jié)
- 函數(shù)式編程的運算不直接操作值,而是由函子完成
- 函子就是一個實現(xiàn)了 map 契約的對象
- 我們可以把函子想象成一個盒子,這個盒子里封裝了一個值
- 想要處理盒子中的值,我們需要給盒子的 map 方法傳遞一個處理值的函數(shù)(純函數(shù)),由這個函數(shù)來對值進行處理
- 最終 map 方法返回一個包含新值的盒子(函子)
可能你不習(xí)慣在代碼中看到new關(guān)鍵字,所以可以在容器中實現(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函子中,很難確認哪一步產(chǎn)生的空值問題。所以就有了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 }) } }
到此這篇關(guān)于JavaScript函數(shù)式編程實現(xiàn)介紹的文章就介紹到這了,更多相關(guān)JS函數(shù)式編程內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Javascript動態(tài)創(chuàng)建表格及刪除行列的方法
這篇文章主要介紹了Javascript動態(tài)創(chuàng)建表格及刪除行列的方法,涉及javascript動態(tài)操作表格的相關(guān)技巧,需要的朋友可以參考下2015-05-05讓html元素隨瀏覽器的大小自適應(yīng)垂直居中的實現(xiàn)方法
下面小編就為大家?guī)硪黄宧tml元素隨瀏覽器的大小自適應(yīng)垂直居中的實現(xiàn)方法。小編覺得挺不錯的,現(xiàn)在就分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2016-10-10JavaScript實現(xiàn)獲取網(wǎng)絡(luò)通信進度
這篇文章主要為大家詳細介紹了如何使用Fetch?API和XMLHttpRequest(XHR)來執(zhí)行網(wǎng)絡(luò)請求,并重點說明如何獲取這兩種方法的網(wǎng)絡(luò)請求進度,感興趣的可以了解下2023-12-12詳解JavaScript如何實現(xiàn)一個簡易的Promise對象
Promise對象的作用將異步操作以同步操作的流程表達出來,避免層層嵌套的回調(diào)函數(shù),而且Promise提供了統(tǒng)一的接口,使得控制異步操作更加容易。本文介紹了如何實現(xiàn)一個簡單的Promise對象,需要的可以參考一下2022-11-11electron-builder 的基本使用及electron打包步驟
electron-builder 作為一個用于 Electron 應(yīng)用程序打包的工具,需要下載并使用 Electron 運行時來創(chuàng)建可執(zhí)行文件,這篇文章主要介紹了electron-builder 的基本使用,需要的朋友可以參考下2023-12-12IE6-IE9使用JSON、table.innerHTML所引發(fā)的問題
這篇文章主要介紹了IE6-IE9使用JSON、table.innerHTML所引發(fā)的問題 ,需要的朋友可以參考下2015-12-12