這是 "函式式 JS" 系列的第二篇。檢視第一篇
簡介
現在我們知道為什麼學習函數語言程式設計實踐可以幫助你成為一個更好的程式設計師了,那讓我們開始一些有趣的東西吧。
在這一部分中,我們將重點關注與函數語言程式設計相關的術語和基本概念。
遺憾的是,這次不會涉及很多程式碼。從好的方面來說,一旦我們理解了術語,我們就能夠方便地討論更復雜的主題。
函式
毋庸置疑,函數語言程式設計中最重要的就是函式。
我們都知道什麼是函式。它基本上就是一個(大多數時候都會有一個名字的)程式碼片段。
function add (x, y) {
return x + y
}
複製程式碼
然而,當談到函數語言程式設計時,我們更想從一個特殊的角度來看待函式:數學中的函式。
數學?沒搞錯吧...
即使我們不必真的去使用代數,我們也不得不承認函數語言程式設計深深植根於數學。
在簡單的數學術語中,函式是一種在給定輸入的情況下產生特定輸出的機器。
有趣的是,給定輸入只能有一個輸出。這意味著,如果我們為函式提供相同的輸入,我們希望它始終做同樣的事情,並且返回相同的值。
這聽起來沒什麼大不了,但實際上這是一個很強的限制。這個數學定義有著深遠的影響:
- 除了輸入(引數)之外,函式不能有其他任何依賴
- 函式必須返回單個值
- 函式必須是確定性的(不能使用隨機值等)
滿足這些標準的函式在程式設計中稱為純函式,它們對於函式式正規化至關重要。
純函式
讓我們看一下 JavaScript 中的一些函式示例,直接體會一下什麼是純函式。
function coin () {
return Math.random() < 0.5 ? 'heads' : 'tails'
}
複製程式碼
Coin
不是純函式,因為它在給定相同輸入(null)的情況下並不總是產生相同的結果 - 它不是確定性的。
let firstName = 'krzysztof'
function uppercaseName (lastName) {
return `${firstName.toUpperCase()} ${lastName.toUpperCase()}`
}
複製程式碼
uppercaseName
不是純函式,因為它依賴於一個不受其控制的變數。我們無法確定在給定相同引數的情況下它總會產生相同的結果。
let user = {
firstName: 'Krzysztof',
age: '26'
}
function happyBirthday () {
user.age = user.age + 1
}
複製程式碼
happyBirthday
不是純函式,因為它不僅訪問了一個不受控制的變數,還不會返回任何內容。
function calculatePrice (unitPrice, noOfUnits, couponValue = 0) {
return unitPrice * noOfUnits - couponValue;
}
複製程式碼
calculatePrice
是純函式。它不使用任何超出其控制範圍的變數,它是確定性的,我們可以非常有信心地說它將始終為相同的輸入引數組合返回相同的結果。
然後呢?
為什麼這一切很重要?有以下一些原因表明純函式比非純函式更有優勢:
- 更易閱讀
你只需要讀一下它的函式體就知道它做了哪些事情。
- 更易理解
不需要查詢外部依賴,函式被呼叫的上下文等。這些對於純函式都沒有任何影響。
- 更易測試
如果你想測試一個純函式,你只需要用一些引數呼叫它,看看結果是否是你想要的結果。根本無需複雜的設定。
- 更高效
如果我們知道對於給定的輸入,函式將始終產生相同的輸出,我們就可以快取(memoize)它的結果,這樣我們就不必在每次呼叫這個函式的時候都重新計算它。
使用純函式可以使程式碼更易於維護 - 因為它可以更輕鬆地管理副作用。在接下來的部分中,我們將瞭解副作用是什麼以及為什麼,遺憾的是,計算機程式中不可能全部都是純函式。
現在我們知道了什麼是純函式,讓我們關注下一個與函式相關的術語:作為一等公民的函式。
一等公民函式
與“純函式”不同,“一等公民函式”在日常工作中並不是一個很實用的概念。但是,在考慮程式語言的特性時,它就很有用了。
如果在一個程式語言中,函式可以像使用其他的值同樣的方式使用,那麼你就可以說這個語言具有“一等公民的函式”,也就是說:
- 它們可以被傳遞,
- 它們可以被分配給變數,
- 它們可以被儲存在更復雜的資料結構中,如陣列或物件。
可以說沒有一等公民的函式,就沒有函數語言程式設計(至少會非常的尷尬)。下面這個例子,說明為什麼函式在 JavaScript 中是一等公民:
function add (a, b) {
return a + b
}
function multiply (a, b) {
return a * b
}
const operations = { // 這裡我們把函式當成普通的值使用
add,
multiply
}
operations.add(1, 2)
複製程式碼
正如上面所說,JavaScript 的函式可以在不同的函式之間傳遞。但是......這麼做的目的是什麼呢?
嗯,將函式傳入和傳出到另外一個函式是函數語言程式設計中的常見做法 - 而且功能非常強大。它給我們引入了...
高階函式
可以“操作”其他函式的函式被稱為高階函式。這裡的操作,意思是指他們可以做到下面兩點中的一個或兩個:
- 把其他函式作為引數,
- 返回一個函式。
這個例子在 JavaScript 世界中很常見。其中一個示例是標準庫中的 Array.prototype.map 函式。它需要一個函式作為引數並將其應用於陣列中的每個元素:
const numbers = [1, 1, 2, 3, 5, 8]
const transformFunction = x => x + 2
numbers.map(transformFunction)
複製程式碼
下面是一個返回函式的函式,這個示例稍顯刻意:
function makeGreeter (greeting) {
return function greet (name) {
return `${greeting}, ${name}!`
}
}
// 或者使用 ES6 的語法:
const makeGreeter = greeting => name => `${greeting}, ${name}!`
const greet = makeGreeter('Hello')
console.log(greet('Krzysztof'))
複製程式碼
你可以看到,這些函式(map 和 makeGreeter)不接受或者返回我們所知道的那些常規的值。他們在操作函式。
你可能已經熟悉了一些高階函式,例如:
- map,
- reduce,
- filter,
- compose,
- forEach,
- … 和別的。
函數語言程式設計就是將一些小型,可重用和通用的函式組合成更復雜的函式。因此,在後面的討論中你將會看到更多不同的高階函式。
那麼,這就是我們開始 FP 之旅所需的所有與函式相關的基本術語了。
下一章,我們將關注函數語言程式設計中的狀態 (state) - 如何管理它,以及如何避免它帶來的問題等等。我們已經提到過一些關於狀態的內容(在討論純函式的時候),後面還有更多!
我們已經學到了不少東西,希望你和我一樣對下一章感到興奮!