JS中的高階函數
高階函數是指以函數作為參數的函數,並且可以將函數作為結果返回的函數。
1. 高階函數
- 接受一個或多個函數作為輸入
- 輸出一個函數
至少滿足以上一個條件的函數
在js的內置對象中同樣存在着一些高階函數,像數組的map
,filter
,reduce
方法等,它們接受一個函數作為參數,並應用這個函數到列表的每一個元素
1.1 map
map
方法接收一個函數作為參數 ,遍歷數組,並且返回一個新的數組,新的數組里的每個元素都執行map
傳入的函數。
let arr = [1, 2, 3, 4];
let arr1 = arr.map(item => item * 2)
console.log(arr1);// [2, 4, 6, 8]
返回的是一個新數組arr1
,不改變原數組
注意
:如果傳入的參數沒有返回值,則數組的每一項都會是undefind
經典題目
console.log(['1','2','3'].map(parseInt));
來看看上面這個代碼輸出什么
答案:[1, NaN, NaN]
解析
parseInt()
函數可解析一個字符串,並返回一個整數。
當參數 radix 的值為 0,或沒有設置該參數時,parseInt()
會根據該字符串來判斷數字的基數。
當忽略參數 radix , 默認的基數如下:
- 如果 字符串 以 “0x” 開頭,
parseInt()
會把 其余部分解析為十六進制的整數。 - 如果字符串以 0 開頭,把其余部分解析為八進制或十六進制的數字。
- 如果字符串以 1 ~ 9 的數字開頭,
parseInt()
將把它解析為十進制的整數
注意
:基數可不是默認十進制噢!
當我們把數組傳入parseInt
時,由於接收2個參數,會將數組的索引作為基數傳個parseInt
,所以實質上進行的是以下幾步
parseInt('1', 0)
parseInt('2', 1)
parseInt('3', 2)
注意
:如果字符串的第一個字符不能被轉換為數字,那么parseInt()
會返回 NaN。
小tips:
parseInt()
還有很多值得注意的問題,可以使用搜索引擎再了解以下
1.2 filter
用於篩選數組
filter
方法接收一個函數作為參數,通過這個函數來指定篩選數組的規則,最后返回滿足規則的新數組
在傳入的函數中有3個參數可選
參數 | 描述 |
---|---|
currentValue | 必須。當前元素的值 |
index | 可選。當前元素的索引值 |
arr | 可選。當前元素屬於的數組對象 |
注意
:
- 不會檢測空數組
- 不會改變原始數組
let arr = [1, 2, 3, 4, 5, 6, 7, 8, 9]
let arr1 = arr.filter(num => {
return num > 5
})
console.log(arr1);// [6, 7, 8, 9]
1.3 reduce
reduce
能做的事情很多,但是我們平時都使用for循環之類的方法代替了,但是reduce
真的高逼格
array.reduce(function(total, currentValue, currentIndex, arr), initialValue)
在w3school
中給出的reduce語法,這里我們常用的只有前面兩個
參數 | 描述 |
---|---|
total | 必需。初始值, 或者計算結束后的返回值。 |
currentValue | 必需。當前元素 |
let arr = [1, 2, 3, 4]
let sum = arr.reduce((value, item) => {
console.log(value, item);
// 1 2 3 3 6 4
return value + item
})
console.log(sum);// 10
從第四行的調試中可以看出reduce
函數的執行過程,在沒有初始值的情況下,將數組第一個值作為value
第二個值作為item
再依次往下遍歷整個數組,將返回值作為value
,數組的下一位作為item
,直至遍歷完成。
利用ruduce
實現數組去重
let arr = [1,1,2,3,4,2,5,4];
let unique = arr.reduce(function (prev, item) {
prev.indexOf(item) === -1 && prev.push(item);
return prev;
}, []);
console.log(unique); // [1, 2, 3, 4, 5]
通過將空數組作為prev
初始值,再通過indexOf
判斷數組中是否包含item
,如果沒有就將item
加入數組,最終返回數組
關於
&&
運算符,第一條語句為true則執行第二條,否則不執行
ruduce
的用法遠不止這些,有興趣的可以再了解以下~
還有很多內置對象都是高階函數,這里就不一一說明了,從上面的三個方法中,已經能很直觀的感受到了函數接收函數作為參數,再返回值的過程,逼格很高也很好用
2. AOP 面向切面編程
當我們需要使用一個公共函數,並且需要在這個函數執行前后添加自己的邏輯,通常我們的做法不能是直接修改這個函數,因為它是公共函數,這時候我們可以通過AOP的方法利用高階函數和原型鏈的特點進行處理
把一些與業務無關的功能抽離出來,通過"動態植入"的方法,摻入到業務邏輯模塊中。這樣做的好處是保證業務邏輯模塊的純凈和高內聚,其次可以方便的復用功能模塊
- 下面我們要實現在函數執行前輸出提示信息
function say(who) {
console.log(who + ':函數執行了');
}
Function.prototype.before = function(callback) {
return (...args) => {
callback()
this(...args)
}
}
let whoSay = say.before(function() {
console.log('你要被調用了');
})
whoSay('ljc')
// 你要被調用了
// ljc:函數執行了
如果需要實現后置通知,只需要將6,7行換以下就可以了
實現的原理
在調用公共函數時,傳入我們需要執行提前執行的函數,在內部函數執行前先調用該函數
3. 偏函數
當一個函數有很多參數時,調用該函數就需要提供多個參數,如果可以減少參數的個數,就能簡化該函數的調用,降低調用該函數的難度。
- 實現3個數求和
function sum(a, b, c){
return a + b + c;
}
sum(1, 2, 3) // 6
在調用時我們需要傳入3個參數,好像有些許麻煩,下面我們用偏函數的做法
創建一個新的partial
函數,這個新函數可以固定住原函數的部分參數,從而減少調用時的輸入的參數,讓我們的調用更加簡單
function sum(a, b, c) {
return a + b + c
}
function partial(sum, c) {
return function (a, b) {
return sum(a, b, c)
}
}
let partialSum = partial(sum, 3)// -> 6
高階函數除了可以接收函數作為參數外,還可以將函數作為結果返回,偏函數就是固定了函數的一個或多個參數,返回一個新的函數接收剩下的參數,以此來簡化函數的調用。
Function.prototype.bind
函數就是一個偏函數的典型代表,它接受的第二個參數開始,為預先添加到綁定函數的參數列表中的參數
4. 函數柯里化
與偏函數不同,柯里化是把接收多個參數的函數轉換成多個只接收一個參數的函數。
我們從一個簡單的例子來認識函數柯里化
function add(a, b) {
return a + b;
}
add(1, 2) // 3 普通做法 一次傳入兩個參數
// 假設有一個 curring 函數可以做到柯里化
function curring(){}
curring(1)(2) // 我們通過這樣的方式來接受參數,這樣就實現了柯里化
接下來我們來看看利用柯里化來實現
function curring(x) {
return return y => x + y
}
curring(1)(2) // => 3
4.1 函數柯里化的作用
要真正理解柯里化還是得看示例
4.1.1 參數復用
我們先看一段短短的代碼,這段代碼中,實現了輸入輸出個人信息的功能,通過myInfo
函數將參數拼接返回,這實際上很簡單,但是當用很多很多的用戶信息時,需要一直傳遞着個人信息
這個參數,這樣顯然是不合理的
function myInfo(inf, name, age) {
return `${inf}:${name}${age}`
}
const myInfo1 = myInfo('個人信息', 'ljc', '19')
console.log(myInfo1); // 個人信息:ljc19
下面我們通過柯里化技術來解決
function myInfoCurry(inf) {
return (name, age) => {
return `${inf}:${name}${age}`
}
}
let myInfoName = myInfoCurry('個人信息')
const myInfo1 = myInfoName('ljc', '19')
const myInfo2 = myInfoName('ljcc','19')
console.log(myInfo2); // 個人信息:ljcc119
console.log(myInfo1); // 個人信息:ljc19
這個就是柯里化技術的作用之一了,參數復用,個人感覺還是很好用的
在上面代碼的基礎上,我們可以繼續擴展我們的信息,就像這樣,利用一個函數就可以實現多個功能
let myInfoSex = myInfoCurry('愛好')
const myInfo3 = myInfoSex('看球賽','聽歌')
console.log(myInfo3); // 愛好:看球賽聽歌
4.1.2 提前返回
這個特性是用來對瀏覽器的監聽事件兼容性做一些判斷並初始化,解決有些瀏覽器對addEventListener
存在的兼容性問題,所以在使用之前做一次判斷,之后就可以省略了
const whichEvent = (function () {
if (window.addEventListener) {
return function (ele, type, listener, useCapture) {
ele.addEventListener(type, function (e) {
listener.call(ele, e)
}, useCapture)
}
} else if (window.attachEvent) {
return function (ele, type, handler) {
ele.attachEvent('on' + type, function (e) {
handler.call(ele, e)
})
}
}
})()
由於使用了立即執行函數,即使觸發多次事件依舊只會觸發一次if條件判斷
4.1.3 延遲執行
下面我們通過一道例題來了解
編寫一個add
函數實現下面的功能
add(1)(2)(3) = 6
add(1, 2, 3)(4) = 10
add(1)(2)(3)(4)(5) = 15
function add(...args) {
let inner = function () {
args.push(...arguments);
inner.toString = function () {
return args.reduce((prev, cur) => {
return prev + cur
})
}
return inner
}
return inner
}
console.log(add(1)(2)(3)); // f 6
這段代碼中涵蓋的知識面很多,核心的部分在於inner.toString
這里,利用了當返回一個函數時返回的是它的字符串形式,所以我們可以利用這個特性來自定義我們的返回值
以上就是關於高階函數的全部內容了,這部分的知識有點難,可能理解的不夠深入,如果有什么好的方法,可以留言討論一下
參考文獻:JavaScript Web前端開發指南
部分代碼實現學習於b站,以及GitHub等平台