內容目錄
Toggle前言
這一篇會介紹非常重要的 JavaScript 函式概念 – 高階函式(Higher-order function)
高階函數是將一個或多個函數作為參數,或將一個函數作為結果回傳的函數。
在本文中,我們將深入探討什麽是高階函數、使用高階函數的好處以及如何在實際應用中使用高階函數。
- 函式導向是什麼?
- 純函式(Pure function)
- 高階函式(Higher-order function)
- 柯理化(Currying)
函式導向是什麼?
所謂的函式導向,一言以蔽之,函式本身就是一個變數。舉例來說:函數本身可以使用const、let、var去做宣告,並且可以變成一個引數(Argument)傳入其他的函式裡面,也可以將他加進陣列、物件裡面。
將函數變成一個引數傳到其他函數
const print = (message) => {
console.log(`print function with ${message}`)
}
const helloMessage = () => {
return "Hello Message"
}
print(helloMessage());
// print function with Hello Message
加入陣列
const array = ["item0", (message) => console.log("I'm item function in array " + message)]
console.log(array[0]);
// item0
array[1]("argument");
// I'm item function in array argument
加入物件
const object = {
helloWorld: "Hello World",
print: (message) => {
console.log(`print function with ${message}`)
}
}
object.print(object.helloWorld);
// print function with Hello World
純函式( Pure function )
當一個函數只受引數( Argument )的影響時,我們稱為純函數( Pure function )。這樣的純函數,因為不會受到其他干擾,這樣的函數具有封裝性,不會受到其他變數以及引數的干擾,也就是會有副作用( Side Effect )。
所謂的副作用( Side Effect )指的是函數在執行過程中產生了外部的變化
- 使用 Date.now() 或是 Math.random()
- 使用 console.log()
- 修改外部資料
- 操作 DOM
- 發起一個 HTTP Request
來看以下的例子,這個例子為一個非純函式,且當修改外部的資料,則函式會受影響
let y = 1;
function xAdd(x) {
return x + y;
};
xAdd(5); //6
可以看到即便是執行了 xAdd(5) 也會因為y的改變,導致執行結果不同。
因此應該將函式變成一個有封裝性,不受外部影響的純函式,純函式的好處除了擁有獨立性以外,也可以更容易寫測試。
function sum(x, y) {
return x + y;
};
sum(1,2); //3
高階函式(Higher-order function)
高階函式(Higher-order function)是指「接受或是回傳函式」的函式。
所謂的接受是指將函式作為引數(Argument)進入一個函式。
回傳函式(Call back function)則是將一個函式作為變數值回傳,有幾種不同類型的高階函數,如 map 和 reduce。
上面的函數導向介紹,就是其中一個例子,讓我們在複習一下。
// Callback function, passed as a parameter in the higher order function
function callbackFunction(){
console.log('I am a callback function');
}
// higher order function
function higherOrderFunction(func){
console.log('I am higher order function')
func()
}
higherOrderFunction(callbackFunction);
在上述程式碼中,higherOrderFunction() 是一個 HOF,因為我們將一個回調函數作為參數傳遞給它。
上面的範例很簡單,讓我們進一步展開,看看如何使用 HOF 編寫更簡潔、更模塊化的代碼。
高階函數的工作原理
假設要寫一個計算圓的面積和直徑的函數。作為剛學程式的人,我們首先想到的解決方案是分別編寫計算面積或直徑的函數。
const radius = [1, 2, 3];
// function to calculate area of the circle
const calculateArea = function (radius) {
const output = [];
for(let i = 0; i< radius.length; i++){
output.push(Math.PI * radius[i] * radius[i]);
}
return output;
}
// function to calculate diameter of the circle
const calculateDiameter = function (radius) {
const output = [];
for(let i = 0; i< radius.length; i++){
output.push(2 * radius[i]);
}
return output;
}
console.log(calculateArea(radius));
console.log(calculateDiameter(radius))
但你注意到上述程式碼的問題嗎?
我們正在重覆寫幾乎相同的函式,但邏輯卻略有不同,而且,我們寫的函式也不能重覆使用,那麽,讓我們看看如何使用高階函數來寫相同的程式碼:
const radius = [1, 2, 3];
// logic to clculate area
const area = function(radius){
return Math.PI * radius * radius;
}
// logic to calculate diameter
const diameter = function(radius){
return 2 * radius;
}
// a reusable function to calculate area, diameter, etc
const calculate = function(radius, logic){
const output = [];
for(let i = 0; i < radius.length; i++){
output.push(logic(radius[i]))
}
return output;
}
console.log(calculate(radius, area));
console.log(calculate(radius, diameter));
正如在上述程式碼中看到的,我們只寫了一個函式 calculate() 來計算圓的面積和直徑。我們只需寫邏輯並將其傳遞給 calculate(),函數就會完成工作。
我們使用高階函式寫的程式碼既簡潔又模組化,每個函式各司其職,我們在這里沒有重覆任何事情。
假設將來我們要寫一個計算圓周長的程式。我們只需寫計算圓周率的邏輯,並將其傳遞給 calculate() 函式即可。
const circumeference = function(radius){
return 2 * Math.PI * radius;
}
console.log(calculate(radius, circumeference));
這裡也提供其他箭頭函式的反例與應用。
const print = (message) => {
console.log(`print function with ${message}`)
}
const helloMessage = () => {
return "Hello Message"
}
print(helloMessage());
// print function with Hello Message
不過高階函式可以讓我們處理更多方便且複雜的情況。
const printNameByCondition = (condition, trueFunc, falseFunc) => {
condition ? trueFunc() : falseFunc();
}
const printHogan = () => console.log("Hello Hogan");
const printBobo = () => console.log("Hello BoBo");
printNameByCondition(true, printHogan, printBobo);
// Hello Hogan
printNameByCondition(false, printHogan, printBobo);
// Hello BoBo
這邊可以看到,我建立了一個函數,裡面有三個引數(Argument),其中後兩者為函數。
透過第一個引述,來去做判斷,如果是true的情況,執行第一個函數,否則就是執行第二個函數。
如何使用高階函數
我們可以使用多種方式來時做高階函數,在處理 array 時,可以使用 map()、reduce()、filter() 和 sort() 函式來處理和轉換陣列中的資料。
高階函數處理物件
可以使用 Object.entries() 函數從對象創建一個新物件。
高階函數使用函式
可以使用 compose() 函式從較簡單的函式創建覆雜的函式。
如何使用一些重要的高階函數
JavaScript 內建的的高階函數有很多,其中最常見的有 map()、filter() 和 reduce()。下面我們就來詳細了解一下它們。
如何在 JavaScript 中使用 map()
map() 函式接收一個陣列,並對陣列中的每個值進行轉換,且不會改變原始陣列。通常用於將數值陣列轉換為具有不同結構的新陣列。
例 1:假設我們要給陣列中的每個元素都加上 10。我們可以使用 map() 方法 reflect 數組中的每個元素,將其加上 10。
const arr = [1, 2, 3, 4, 5];
const output = arr.map((num) => num += 10)
console.log(arr); // [1, 2, 3, 4, 5]
console.log(output); // [11, 12, 13, 14, 15]
在上面的範例中,arr 是一個包含五個元素的陣列:
我們使用 map 方法對陣列中的每個元素應用一個函式,然後回傳一個包含修改後元素的新陣列。
傳遞給 map 的 call back function 使用箭頭函式,接收一個參數 num。
該函式將 10 加到 num(數組中的每個元素),並回傳結果。
例 2:這里有一個使用者陣列。假設我們只需要使用者的姓名,我們只需使用 map() 方法從 users 陣列中提取即可。
const users = [
{firstName: 'John', lastName: 'Doe', age: 25},
{firstName: 'Jane', lastName: 'Doe', age: 30},
{firstName: 'Jack', lastName: 'Doe', age: 35},
{firstName: 'Jill', lastName: 'Doe', age: 40},
{firstName: 'Joe', lastName: 'Doe', age: 45},
]
const result = users.map((user) => user.firstName + ' ' + user.lastName)
console.log(result); // ['John Doe', 'Jane Doe', 'Jack Doe', 'Jill Doe', 'Joe Doe']
在上述程式碼中,users 是一個代表使用者的物件陣列。每個物件有三個屬性:姓、名、年齡。
我們使用 map() 方法對每個用戶進行映射,以拿到屬性 firstName 和 lastName。
Call back function 接收一個參數 user,它代表 users 陣列(一個對象)中的一個元素。
該函式將使用者的 name 和 lastName 屬性連接起來,並回傳結果。
如何在 JavaScript 中使用 filter()
filter() 函數接收一個陣列並回傳一個新陣列,其中只包含符合特定條件的值。
它也不會改變原始陣列,通常用於根據特定條件從陣列中選擇資料集。
例 1:使用 filter() 函數可以從數字陣列中回傳奇數。
const arr = [1, 2, 3, 4, 5];
const output = arr.filter((num) => num % 2) // filter out odd numbers
console.log(arr); // [1, 2, 3, 4, 5]
console.log(output); // [1, 3, 5]
在上述代碼中,arr 是一個包含五個元素的陣列:
filter 是一種方法,用於創建一個新陣列,其中的元素必須通過所提供的回調函數中指定的測試。
該回調函數通過檢查 num 是否能被 2 整除(num % 2)來檢查 num 是否為奇數。如果 num 不能被 2 整除,函式回傳 true,否則回傳 false。
當在 arr 上使用 filter 時,會對數列中的每個元素使用該函式,創建一個新數列,其中只包含回傳 true 或通過指定條件的元素。原數列保持不變,並回傳結果。
例 2:可以使用 filter() 在數列中只回傳年齡大於 30 歲的用戶。
const users = [
{firstName: 'John', lastName: 'Doe', age: 25},
{firstName: 'Jane', lastName: 'Doe', age: 30},
{firstName: 'Jack', lastName: 'Doe', age: 35},
{firstName: 'Jill', lastName: 'Doe', age: 40},
{firstName: 'Joe', lastName: 'Doe', age: 45},
]
// Find the users with age greater than 30
const output = users.filter(({age}) => age > 30)
console.log(output); // [{firstName: 'Jack', lastName: 'Doe', age: 35}, {firstName: 'Jill', lastName: 'Doe', age: 40}, {firstName: 'Joe', lastName: 'Doe', age: 45}]
在上述程式碼中,users 是一個代表用戶的陣列。每個物件有三個屬性:名、姓和年齡。
在 users 數組上使用 filter,並對陣列中的每個元素使用回調函數。
函式接收一個參數,換句話說,一個被重組為單個屬性 age 的對象。
該函式檢查年齡是否大於 30 歲。如果是的話,函數回傳 true,否則回傳 false。
在對 users 使用 filter 時,它會對陣列中的每個元素使用此函式,創建一個新陣列,其中只包含傳給函式時回傳 true 的元素,並回傳結果。原始的 users 陣列保持不變。
如何在 JavaScript 中使用 reduce()
reduce() 在滿多人心中,是相對複雜的函式,如果以前接觸過 reduce() 方法,但一開始無法理界,那麼就繼續看下去吧!
我們可能會有一個疑問:為什麽要使用 reduce() ?因為已經有很多好用的函式了,我們如何決定使用哪一個,以及何時使用?
就 reduce() 而言,當你想對陣列元素執行某種操作並回傳一個單一值時,就應該使用它。
單一值 :指的是對陣列元素重覆應用函數後的累積結果。
例如,您可以使用 reduce() 求數列中所有元素的總和、查找最大值或最小值、將多個對象合併成一個對象,或對數列中的不同元素進行分割。我們來透過範例了解這些。
例 1:使用 reduce() 求數組中所有元素的和:
const numbers = [1, 2, 3, 4, 5];
const sum = numbers.reduce((total, currentValue) => {
return total + currentValue;
}, 0)
console.log(sum); // 15
在這個範例中,reduce() 方法在數列中使用,並通過一個回調函數,該函數有兩個參數:total 和 currentValue。
total 參數是使用函數時,回傳值的累加,而 currentValue 是數列中正在處理的當前元素。
reduce() 的第二個參數也是初始值,在以上範例中為 0,用作第一次疊代時 total 的初始值。
在每次疊代中,函數都會將當前值與總數相加,並回傳總數的新值。
接下來,reduce() 方法使用回傳值作為下一次疊代的總數,直到處理完數列中的所有元素。
最後,它會回傳總數的最終值,即數列中所有元素的總和。
例 2:使用 reduce() 求數列中的最大值:
let numbers = [5, 20, 100, 60, 1];
const maxValue = numbers.reduce((max, curr) => {
if(curr > max) max = curr;
return max;
});
console.log(maxValue); // 100
在此範例中,我們再次在回調函數中使用了 max 和 curr 這兩個參數,這次我們沒有在 reduce() 方法中傳遞第二個參數。因此,默認值將是數列中的第一個元素。
回調函數首先檢查當前元素 curr 是否大於當前最大值 max。如果是,它就會更新 max 的值,使其成為當前元素。如果不是,則不更新 max。最後,函式回傳 max 的值。
在本例中,reduce() 方法首先會將 max 設置為 5,將 curr 設置為 20。然後,它會檢查 20 是否大於 5,結果是大於 5,因此會將 max 更新為 20。
然後將 curr 設置為 100,並檢查 100 是否大於 20,結果是大於 20,因此將 max 更新為 100。
這個過程一直持續到處理完數組中的所有元素為止。max 的最終值將是數列中的最大值,在此範例中就是 100。
高階函數的優點
使用高階函數對開發者有一些重要的好處。
首先,高階函數可以使程式碼更加簡潔易懂,有助於提高程式碼的可讀性,有助於加快開發過程,並使程式碼使用更加容易。
其次,高階函數可以幫助將程式碼吃組話,使其更易於維護和擴展。
柯理化(Currying)
在高階函式中,柯理化(Currying)是一個特殊且重要的技巧。
可以分別將一個高階函式中,不同階層的引數傳入,也可以透過此概念,為一個高階函數新增前綴(prefix)
const userLogs = userName => message => console.log(`${userName} -> ${message}`)
const log1 = userLogs("Hogan");
log1("Hello World");
log1("Hello");
const log2 = userLogs("Bobo");
log2("Hello World");
log2("Hello");
這邊也使用一個範例來實作,其中也有給了兩個不同的高階函式的前綴(prefix)
結語
本文探討了函式導向的概念、什麽是高階函數、使用高階函數的好處以及如何在實際應用中使用高階函數,另外也用了比較小的篇幅介紹了柯理化(Curring)是一個什麼樣的功能,也針對純函數(Pure Function)做了一個介紹。
通過使用高階函數,開發者可以更聰明地寫程式,將程式碼模組化,使程式碼更清晰、更易於使用。
如果有任何建議與疑問也歡迎留言!
如果喜歡此系列文章,請不吝於按下喜歡及分享,讓更多人看到唷~
引用
React白話文運動05-高階函式(Higher-order function)
其他文章參考
JavaScript Async Await – React 白話文運動 03
JavaScript ES6 Object – React 白話文運動 02