前端必知道的幾個 JavaScript 高級函數(shù)
高階函數(shù)是對其他函數(shù)進行操作的函數(shù),可以將它們作為參數(shù)或通過返回它們。簡單來說,高階函數(shù)是一個函數(shù),它接收函數(shù)作為參數(shù)或?qū)⒑瘮?shù)作為輸出返回。
例如?Array.prototype.map,Array.prototype.filter,Array.prototype.reduce?都是一些高階函數(shù)。
尾調(diào)用和尾遞歸
尾調(diào)用(Tail Call)是函數(shù)式編程的一個重要概念,本身非常簡單,一句話就能說清楚。就是指某個函數(shù)的最后一步是調(diào)用另一個函數(shù)。
function g(x) {
console.log(x)
}
function f(x) {
return g(x)
}
console.log(f(1))
//上面代碼中,函數(shù)f的最后一步是調(diào)用函數(shù)g,這就是尾調(diào)用。上面代碼中,函數(shù) f 的最后一步是調(diào)用函數(shù) g,這就是尾調(diào)用。尾調(diào)用不一定出現(xiàn)在函數(shù)尾部,只要是最后一步操作即可。
函數(shù)調(diào)用自身,稱為遞歸。如果尾調(diào)用自身,就稱為尾遞歸。遞歸非常耗費內(nèi)存,因為需要同時保存成千上百個調(diào)用幀,很容易發(fā)生棧溢出錯誤。但是隊伍尾遞歸來說,由于只存在一個調(diào)用幀,所以永遠不會發(fā)生棧溢出錯誤。
function factorial(n) {
if (n === 1) {
return 1
}
return n * factorial(n - 1)
}上面代碼是一個階乘函數(shù),計算 n 的階乘,最多需要保存 n 個調(diào)用數(shù)據(jù),復雜度為 O(n),如果改寫成尾調(diào)用,只保留一個調(diào)用記錄,復雜度為 O(1)。
function factor(n, total) {
if (n === 1) {
return total
}
return factor(n - 1, n * total)
}斐波拉切數(shù)列也是可以用于尾調(diào)用。
function Fibonacci(n) {
if (n <= 1) {
return 1
}
return Fibonacci(n - 1) + Fibonacci(n - 2)
}
//尾遞歸
function Fibona(n, ac1 = 1, ac2 = 1) {
if (n <= 1) {
return ac2
}
return Fibona(n - 1, ac2, ac1 + ac2)
}柯理化函數(shù)
在數(shù)學和計算機科學中,柯里化是一種將使用多個參數(shù)的一個函數(shù)轉(zhuǎn)換成一系列使用一個參數(shù)的函數(shù)的技術。所謂柯里化就是把具有較多參數(shù)的函數(shù)轉(zhuǎn)換成具有較少參數(shù)的函數(shù)的過程。?
舉個例子
//普通函數(shù)
function fn(a, b, c, d, e) {
console.log(a, b, c, d, e)
}
//生成的柯里化函數(shù)
let _fn = curry(fn)
_fn(1, 2, 3, 4, 5) // print: 1,2,3,4,5
_fn(1)(2)(3, 4, 5) // print: 1,2,3,4,5
_fn(1, 2)(3, 4)(5) // print: 1,2,3,4,5
_fn(1)(2)(3)(4)(5) // print: 1,2,3,4,5柯理化函數(shù)的實現(xiàn)
// 對求和函數(shù)做curry化
let f1 = curry(add, 1, 2, 3)
console.log('復雜版', f1()) // 6
// 對求和函數(shù)做curry化
let f2 = curry(add, 1, 2)
console.log('復雜版', f2(3)) // 6
// 對求和函數(shù)做curry化
let f3 = curry(add)
console.log('復雜版', f3(1, 2, 3)) // 6
// 復雜版curry函數(shù)可以多次調(diào)用,如下:
console.log('復雜版', f3(1)(2)(3)) // 6
console.log('復雜版', f3(1, 2)(3)) // 6
console.log('復雜版', f3(1)(2, 3)) // 6
// 復雜版(每次可傳入不定數(shù)量的參數(shù),當所傳參數(shù)總數(shù)不少于函數(shù)的形參總數(shù)時,才會執(zhí)行)
function curry(fn) {
// 閉包
// 緩存除函數(shù)fn之外的所有參數(shù)
let args = Array.prototype.slice.call(arguments, 1)
return function() {
// 連接已緩存的老的參數(shù)和新傳入的參數(shù)(即把每次傳入的參數(shù)全部先保存下來,但是并不執(zhí)行)
let newArgs = args.concat(Array.from(arguments))
if (newArgs.length < fn.length) {
// 累積的參數(shù)總數(shù)少于fn形參總數(shù)
// 遞歸傳入fn和已累積的參數(shù)
return curry.call(this, fn, ...newArgs)
} else {
// 調(diào)用
return fn.apply(this, newArgs)
}
}
}柯里化的用途
柯里化實際是把簡答的問題復雜化了,但是復雜化的同時,我們在使用函數(shù)時擁有了更加多的自由度。而這里對于函數(shù)參數(shù)的自由處理,正是柯里化的核心所在。柯里化本質(zhì)上是降低通用性,提高適用性。來看一個例子:
我們工作中會遇到各種需要通過正則檢驗的需求,比如校驗電話號碼、校驗郵箱、校驗身份證號、校驗密碼等, 這時我們會封裝一個通用函數(shù) checkByRegExp ,接收兩個參數(shù),校驗的正則對象和待校驗的字符串
function checkByRegExp(regExp, string) {
return regExp.text(string)
}
checkByRegExp(/^1\d{10}$/, '18642838455') // 校驗電話號碼
checkByRegExp(/^(\w)+(\.\w+)*@(\w)+((\.\w+)+)$/, '[email protected]') // 校驗郵箱我們每次進行校驗的時候都需要輸入一串正則,再校驗同一類型的數(shù)據(jù)時,相同的正則我們需要寫多次, 這就導致我們在使用的時候效率低下,并且由于 checkByRegExp 函數(shù)本身是一個工具函數(shù)并沒有任何意義。此時,我們可以借助柯里化對 checkByRegExp 函數(shù)進行封裝,以簡化代碼書寫,提高代碼可讀性。
//進行柯里化
let _check = curry(checkByRegExp)
//生成工具函數(shù),驗證電話號碼
let checkCellPhone = _check(/^1\d{10}$/)
//生成工具函數(shù),驗證郵箱
let checkEmail = _check(/^(\w)+(\.\w+)*@(\w)+((\.\w+)+)$/)
checkCellPhone('18642838455') // 校驗電話號碼
checkCellPhone('13109840560') // 校驗電話號碼
checkCellPhone('13204061212') // 校驗電話號碼
checkEmail('[email protected]') // 校驗郵箱
checkEmail('[email protected]') // 校驗郵箱
checkEmail('[email protected]') // 校驗郵箱柯里化函數(shù)參數(shù) length
函數(shù) currying 的實現(xiàn)中,使用了 fn.length 來表示函數(shù)參數(shù)的個數(shù),那 fn.length 表示函數(shù)的所有參數(shù)個數(shù)嗎?并不是。
函數(shù)的 length 屬性獲取的是形參的個數(shù),但是形參的數(shù)量不包括剩余參數(shù)個數(shù),而且僅包括第一個具有默認值之前的參數(shù)個數(shù),看下面的例子。
((a, b, c) => {}).length
// 3
((a, b, c = 3) => {}).length
// 2
((a, b = 2, c) => {}).length
// 1
((a = 1, b, c) => {}).length
// 0
((...args) => {}).length
// 0
const fn = (...args) => {
console.log(args.length)
}
fn(1, 2, 3)
// 3compose 函數(shù)
compose 就是組合函數(shù),將子函數(shù)串聯(lián)起來執(zhí)行,一個函數(shù)的輸出結(jié)果是另一個函數(shù)的輸入?yún)?shù),一旦第一個函數(shù)開始執(zhí)行,會像多米諾骨牌一樣推導執(zhí)行后續(xù)函數(shù)。
const greeting = name => `Hello ${name}`
const toUpper = str => str.toUpperCase()
toUpper(greeting('Onion')) // HELLO ONIONcompose 函數(shù)的特點
compose 接受函數(shù)作為參數(shù),從右向左執(zhí)行,返回類型函數(shù)
fn()全部參數(shù)傳給最右邊的函數(shù),得到結(jié)果后傳給倒數(shù)第二個,依次傳遞
compose 的實現(xiàn)
var compose = function(...args) {
var len = args.length // args函數(shù)的個數(shù)
var count = len - 1
var result
return function func(...args1) {
// func函數(shù)的args1參數(shù)枚舉
result = args[count].call(this, args1)
if (count > 0) {
count--
return func.call(null, result) // result 上一個函數(shù)的返回結(jié)果
} else {
//回復count初始狀態(tài)
count = len - 1
return result
}
}
}舉個例子
var greeting = (name) => `Hello ${name}`
var toUpper = str => str.toUpperCase()
var fn = compose(toUpper, greeting)
console.log(fn('jack'))大家熟悉的 webpack 里面的 loader 執(zhí)行順序是從右到左,是因為webpack 選擇的是 compose 方式,從右到左依次執(zhí)行 loader,每個 loader 是一個函數(shù)。
rules: [
{ test: /\.css$/, use: ['style-loader', 'css-loader'] }
]如上,webpack 使用了 style-loader 和 css-loader,它是先用 css-loader 加載.css 文件,然后 style-loader 將內(nèi)部樣式注入到我們的 html 頁面。
webpack 里面的 compose 代碼如下:
const compose = (...fns) => {
return fns.reduce(
(prevFn, nextFn) => {
return value =>prevFn(nextFn(value))
},
value => value
)
}