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