摘要:在這篇文章中,我將通過中的大量代碼示例向您詳細介紹函數式編程和一些重要概念。注意在函數式編程中不鼓勵可變性。純函數是穩定,一致并且可預測的。
原文:Functional Programming Principles in Javascript
作者:TK
譯者:博軒
經過很長一段時間的學習和面向對象編程的工作,我退后一步,開始思考系統的復雜性。
“復雜性是任何使軟件難以理解或修改的東西。” - John Outerhout
做了一些研究,我發現了函數式編程概念,如不變性和純函數。 這些概念使你能夠構建無副作用的功能,而函數式編程的一些優點,也使得系統變得更加容易維護。
在這篇文章中,我將通過 JavaScript 中的大量代碼示例向您詳細介紹函數式編程和一些重要概念。
什么是函數式編程?維基百科:Functional programming
函數式編程是一種編程范式,一種構建計算機程序結構和元素的方式,將計算視為數學函數的評估并避免改變狀態和可變數據 -- 維基百科純函數
當我們想要理解函數式編程時,我們學到的第一個基本概念是純函數。 那么我們怎么知道函數是否純粹呢? 這是一個非常嚴格的純度定義:
如果給出相同的參數,它返回相同的結果(它也稱為確定性)
它不會引起任何可觀察到的副作用
如果給出相同的參數,它返回相同的結果我們想要實現一個計算圓的面積的函數。 不純的函數將接收半徑:radius 作為參數,然后計算 radius * radius * PI :
const PI = 3.14; const calculateArea = (radius) => radius * radius * PI; calculateArea(10); // returns 314
為什么這是一個不純的功能? 僅僅因為它使用的是未作為參數傳遞給函數的全局對象。
想象一下,數學家認為 PI 值實際上是 42, 并且改變了全局對象的值。
不純的函數現在將導致 10 * 10 * 42 = 4200 .對于相同的參數(radius= 10),我們得到不同的結果。
我們來解決它吧!
const PI = 3.14; const calculateArea = (radius, pi) => radius * radius * pi; calculateArea(10, PI); // returns 314
現在我們將 PI 的值作為參數傳遞給函數。 所以現在我們只是訪問傳遞給函數的參數。 沒有外部對象(參數)。
對于參數 radius = 10 和 PI = 3.14,我們將始終具有相同的結果:314
對于參數 radius = 10 和 PI = 42,我們將始終具有相同的結果:4200
讀取文件 (Node.js)如果我們的函數讀取外部文件,它也不是純函數 - 文件的內容可以更改:
const fs = require("fs"); const charactersCounter = (text) => `Character count: ${text.length}`; function analyzeFile(filepath) { let fileContent = fs.readFileSync(filepath); return charactersCounter(fileContent); }生成隨機數
任何依賴于隨機數生成器的函數都不可能是純函數:
function yearEndEvaluation() { if (Math.random() > 0.5) { return "You get a raise!"; } else { return "Better luck next year!"; } }它不會引起任何可觀察到的副作用
什么是可觀察副作用呢?其中一種示例,就是在函數內修改全局的對象,或者參數。
現在我們要實現一個函數,來接收一個整數值并返回增加 1 的值。
let counter = 1; function increaseCounter(value) { counter = value + 1; } increaseCounter(counter); console.log(counter); // 2
我們首先定義了變量 counter 。 然后使用不純的函數接收該值并重新為 counter 賦值,使其值增加 1 。
注意:在函數式編程中不鼓勵可變性。
上面的例子中,我們修改了全局對象。 但是我們如何才能讓函數變得純凈呢? 只需返回增加 1 的值。
let counter = 1; const increaseCounter = (value) => value + 1; increaseCounter(counter); // 2 console.log(counter); // 1
可以看到我們的純函數 increaseCounter 返回 2 ,但是 counter 還保持之前的值。該函數會使返回的數字遞增,而且不更改變量的值。
如果我們遵循這兩個簡單的規則,就會使我們的程序更加容易理解。每個功能都是孤立的,無法影響到我們的系統。
純函數是穩定,一致并且可預測的。給定相同的參數,純函數將始終返回相同的結果。我們不需要考慮,相同的參數會產生不同的結果,因為它永遠不會發生。
純函數的好處 容易測試純函數的代碼更加容易測試。我們不需要模擬任何執行的上下文。我們可以使用不同的上下文對純函數進行單元測試:
給定參數 A -> 期望函數返回 B
給定參數 C -> 期望函數返回 D
一個簡單的例子,函數接收一個數字集合,并期望數字集合每個元素遞增。
let list = [1, 2, 3, 4, 5]; const incrementNumbers = (list) => list.map(number => number + 1);
我們接收到數字數組,使用 map 遞增每個數字,并返回一個新的遞增數字列表。
incrementNumbers(list); // [2, 3, 4, 5, 6]
對于輸入 [1, 2, 3, 4, 5],預期輸出將是 [2, 3, 4, 5, 6]。
不變性隨著時間的推移不變,或無法改變
當數據具有不可變性時,它的狀態在創建之后,就不能改變了。你不能去更改一個不可變的對象,但是你可以使用新值去創建一個新的對象。
在 JavaScript 中,我們常使用 for 循環。下面這個 for 循環有一些可變的變量。
var values = [1, 2, 3, 4, 5]; var sumOfValues = 0; for (var i = 0; i < values.length; i++) { sumOfValues += values[i]; } sumOfValues // 15
對于每次迭代,我們都在改變變量 i 和 sumOfValues 的狀態。但是我們要如何處理迭代中的可變性?使用遞歸
let list = [1, 2, 3, 4, 5]; let accumulator = 0; function sum(list, accumulator) { if (list.length == 0) { return accumulator; } // 移除數組第一項,并做累加 return sum(list.slice(1), accumulator + list[0]); } sum(list, accumulator); // 15 list; // [1, 2, 3, 4, 5] accumulator; // 0
所以這里我們有 sum 函數接收數值向量。 該函數調用自身,直到我們將列表清空。 對于每個“迭代”,我們會將該值添加到總累加器。
使用遞歸,我們可以保持變量的不可變性。 列表和累加器變量不會更改,會保持相同的值。
注意:我們可以使用reduce來實現這個功能。 我們將在高階函數主題中介紹這個話題。
構建對象的最終狀態也很常見。想象一下,我們有一個字符串,我們想將這個字符串轉換為 url slug。
在 Ruby 中的面向對象編程中,我們將創建一個類,比方說,UrlSlugify。 這個類將有一個 slugify 方法將字符串輸入轉換為 url slug 。
class UrlSlugify attr_reader :text def initialize(text) @text = text end def slugify! text.downcase! text.strip! text.gsub!(" ", "-") end end UrlSlugify.new(" I will be a url slug ").slugify! # "i-will-be-a-url-slug"
他已經實現了!(It’s implemented!)
這里我們使用命令式編程,準確的說明我們想要在 函數實現的過程中(slugify)每一步要做什么:首先是轉換成小寫,然后移除無用的空格,最后用連字符替換剩余的空格。
但是,在這個過程中,函數改變了輸入的參數。
我們可以通過執行函數組合或函數鏈來處理這種變異。 換句話說,函數的結果將用作下一個函數的輸入,而不修改原始輸入字符串。
let string = " I will be a url slug "; function slugify(string) { return string.toLowerCase() .trim() .split(" ") .join("-"); } slugify(string); // i-will-be-a-url-slug
這里我們:
toLowerCase:將字符串轉換為全部小寫
trim:從字符串的兩端刪除空格
split 和 join :用給定字符串中的替換替換所有匹配實例
我們將所有這四個功能結合起來,就可以實現 slugify 的功能了。
參考透明度維基百科:Referential transparency
如果表達式可以替換為其相應的值而不更改程序的行為,則該表達式稱為引用透明。這要求表達式是純粹的,也就是說相同輸入的表達式值必須相同,并且其評估必須沒有副作用。-- 維基百科
讓我們實現一個計算平方的方法:
const square = (n) => n * n;
在給定相同輸入的情況下,此純函數將始終具有相同的輸出。
square(2); // 4 square(2); // 4 square(2); // 4 // ...
把 2 傳遞給 square 方法將始終返回 4。所以,現在我們可以使用 4 來替換 square(2)。我們的函數是引用透明的。
基本上,如果函數對同一輸入始終產生相同的結果,則引用透明。
pure functions + immutable data = referential transparency
純函數 + 不可變數據 = 參照透明度
有了這個概念,我們可以做一件很 cool 的事情,就是使這個函數擁有記憶(memoize)。
想象一下我們擁有這樣一個函數:
const sum = (a, b) => a + b;
我們用這些參數調用它:
sum(3, sum(5, 8));
sum(5, 8) 等于 13。這個函數總是返回 13。因此,我們可以這樣做:
sum(3, 13);
這個表達式總是會返回 16 。我們可以用一個數值常量替換整個表達式,并記住它。
這里推薦一篇淘寶FED關于 memoize 的文章:性能優化:memoization函數是一等公民
函數作為一等公民,意味著函數也可以視為值處理,并當做數據來使用。
函數作為一等公民有如下特性:
可以當做常量,或者變量來引用
將函數當做參數傳遞給其他函數
將函數作為其他函數的返回值
我們的想法是函數視為值并將它們作為參數傳遞。 這樣我們就可以組合不同的函數來創建具有新行為的新函數。
想象一下,我們有一個函數可以將兩個值相加,然后將該值加倍:
const doubleSum = (a, b) => (a + b) * 2;
現在是一個,將兩值相減,并返回該值加倍的函數:
const doubleSubtraction = (a, b) => (a - b) * 2;
這些函數具有相似的邏輯,但是計算時的運算符不同。 如果我們可以將函數視為值并將它們作為參數傳遞,我們可以構建一個函數來接收運算符函數并在函數中使用它。
const sum = (a, b) => a + b; const subtraction = (a, b) => a - b; const doubleOperator = (f, a, b) => f(a, b) * 2; doubleOperator(sum, 3, 1); // 8 doubleOperator(subtraction, 3, 1); // 4
現在我們有一個函數參數:f,并用它來處理 a 和 b 。 我們傳遞了 sum 和 subtraction 函數以使用 doubleOperator 函數進行組合并創建一個新行為。
高階函數維基百科:Higher-order function
當我們談論高階函數時,通常是指一個函數同時具有:
將一個或多個函數作為參數,或
返回一個函數作為結果
我們上面實現的 doubleOperator 函數是一個高階函數,因為它將一個運算符函數作為參數并使用它。
您可能已經聽說過 filter,map 和 reduce 。 我們來看看這些。
Filter給定一個集合,我們希望按照屬性進行過濾。filter 函數需要 true 或者 false 值來確定元素是否應該包含在結果集合中。基本上,如果回調表達式返回的是 true ,filter 函數返回的結果會包含該元素。否則,就不會包含該元素。
一個簡單的例子是當我們有一個整數集合時,我們只想要過濾偶數。
命令式編程使用 JavaScript 來實現時,需要如下操作:
創建一個空數組 evenNumbers
迭代數字數組
將偶數推到 evenNumbers 數組
var numbers = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; var evenNumbers = []; for (var i = 0; i < numbers.length; i++) { if (numbers[i] % 2 == 0) { evenNumbers.push(numbers[i]); } } console.log(evenNumbers); // (6) [0, 2, 4, 6, 8, 10]
我們還可以使用 filter 高階函數來接收 even 函數,并返回偶數列表:
const even = n => n % 2 == 0; const listOfNumbers = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; listOfNumbers.filter(even); // [0, 2, 4, 6, 8, 10]
我在Hacker Rank FP上解決的一個有趣問題是Filter Array問題。 問題的想法是過濾給定的整數數組,并僅輸出那些小于指定值X的值。
針對此問題,命令式JavaScript解決方案如下:
var filterArray = function(x, coll) { var resultArray = []; for (var i = 0; i < coll.length; i++) { if (coll[i] < x) { resultArray.push(coll[i]); } } return resultArray; } console.log(filterArray(3, [10, 9, 8, 2, 7, 5, 1, 3, 0])); // (3) [2, 1, 0]
我們的函數會做如下的事情 - 迭代集合,將集合當前項與 x 進行比較,如果它符合條件,則將此元素推送到 resultArray。
聲明式編程但我們想要一種更具聲明性的方法來解決這個問題,并使用過濾器高階函數。
聲明式 JavaScript 解決方案將是這樣的:
function smaller(number) { return number < this; } function filterArray(x, listOfNumbers) { return listOfNumbers.filter(smaller, x); } let numbers = [10, 9, 8, 2, 7, 5, 1, 3, 0]; filterArray(3, numbers); // [2, 1, 0]
在 smaller 函數中使用 this 首先看起來有點奇怪,但很容易理解。
this 將作為第二個參數傳給 filter 方法。在這個示例中,3(x)代表 this。
這樣的操作也可以用于集合。 想象一下,我們有一個人物集合,包含了 name、 age 屬性。
let people = [ { name: "TK", age: 26 }, { name: "Kaio", age: 10 }, { name: "Kazumi", age: 30 } ];
我們希望僅過濾指定年齡值的人,在此示例中,年齡超過18歲的人。
const olderThan18 = person => person.age > 18; const overAge = people => people.filter(olderThan18); overAge(people); // [{ name: "TK", age: 26 }, { name: "Kazumi", age: 30 }]
代碼摘要:
我們有一份人員名單(姓名和年齡)。
我們有一個函數 oldThan18。在這種情況下,對于 people 數組中的每個人,我們想要訪問年齡并查看它是否超過 18 歲。
我們根據此功能過濾所有人。
Mapmap 的概念是轉換一個集合。
map 方法會將集合傳入函數,并根據返回的值構建新集合。
讓我們使用剛才的 people 集合。我們現在不想過濾年齡了。我們只想得到一個列表,元素就像:TK is 26 years old。所以最后的字符串可能是 :name is:age years old 其中 :name 和 :age 是 people 集合中每個元素的屬性。
下面是使用命令式 JavaScript 編碼的示例:
var people = [ { name: "TK", age: 26 }, { name: "Kaio", age: 10 }, { name: "Kazumi", age: 30 } ]; var peopleSentences = []; for (var i = 0; i < people.length; i++) { var sentence = people[i].name + " is " + people[i].age + " years old"; peopleSentences.push(sentence); } console.log(peopleSentences); // ["TK is 26 years old", "Kaio is 10 years old", "Kazumi is 30 years old"]
下面是使用聲明式 JavaScript 編碼的示例:
const makeSentence = (person) => `${person.name} is ${person.age} years old`; const peopleSentences = (people) => people.map(makeSentence); peopleSentences(people); // ["TK is 26 years old", "Kaio is 10 years old", "Kazumi is 30 years old"]
要做的事情是將給定數組轉換為新數組。
另一個有趣的 Hacker Rank 問題是更新列表問題。 我們只想用它們的絕對值更新給定數組的值。
例如,輸入 [1,2,3-4,5] 需要輸出為 [1,2,3,4,5] 。 -4 的絕對值是 4。
一種簡單的解決方案是將每個集合的值進行就地更新 (in-place)。
var values = [1, 2, 3, -4, 5]; for (var i = 0; i < values.length; i++) { values[i] = Math.abs(values[i]); } console.log(values); // [1, 2, 3, 4, 5]
我們使用 Math.abs 函數將值轉換為其絕對值,并進行就地更新。
這不是一個函數式的解決方案。
首先,我們了解了不變性。 我們知道不可變性對于使我們的函數更加一致和可預測非常重要。 我們的想法是建立一個具有所有絕對值的新集合。
第二,為什么不在這里使用 map 來轉換所有數據?
我的第一個想法是測試 Math.abs 函數只處理一個值。
Math.abs(-1); // 1 Math.abs(1); // 1 Math.abs(-2); // 2 Math.abs(2); // 2
我們希望將每個值轉換為正值(絕對值)。
現在我們知道如何對一個值進行取絕對值的操作,我們可以將這個函數通過參數的方式傳遞給 map 。你還記得高階函數可以接收函數作為參數并使用它嗎? 是的,map 可以。
let values = [1, 2, 3, -4, 5]; const updateListMap = (values) => values.map(Math.abs); updateListMap(values); // [1, 2, 3, 4, 5]
Wow,鵝妹子嚶!
Reducereduce 函數的概念是,接收一個函數和一個集合,然后組合他們來創建返回值。
一個常見的例子是獲得訂單的總金額。想象一下,你正在一個購物網站購物。你增加了 Product 1,Product 2,Product 3,Product 4 到你的購物車。現在我們要計算購物車的總金額。
使用命令式編程的方式,我們將迭代訂單列表并將每個產品金額與總金額相加。
var orders = [ { productTitle: "Product 1", amount: 10 }, { productTitle: "Product 2", amount: 30 }, { productTitle: "Product 3", amount: 20 }, { productTitle: "Product 4", amount: 60 } ]; var totalAmount = 0; for (var i = 0; i < orders.length; i++) { totalAmount += orders[i].amount; } console.log(totalAmount); // 120
使用 reduce ,我們可以創建一個用來處理累加的函數,并將其作為參數傳給 reduce 函數。
let shoppingCart = [ { productTitle: "Product 1", amount: 10 }, { productTitle: "Product 2", amount: 30 }, { productTitle: "Product 3", amount: 20 }, { productTitle: "Product 4", amount: 60 } ]; const sumAmount = (currentTotalAmount, order) => currentTotalAmount + order.amount; const getTotalAmount = (cart) => cart.reduce(sumAmount, 0); getTotalAmount(shoppingCart); // 120
這里我們有 shoppingCart,sumAmount函數接收當前的 currentTotalAmount ,對所有訂單進行累加。
getTotalAmount 函數會接收 sumAmount 函數 從 0 開始累加購物車的值。
獲得總金額的另一種方法是組合使用 map 和 reduce。 那是什么意思? 我們可以使用 map 將 shoppingCart 轉換為 amount 值的集合,然后只使用 reduce 函數和 sumAmount 函數。
const getAmount = (order) => order.amount; const sumAmount = (acc, amount) => acc + amount; function getTotalAmount(shoppingCart) { return shoppingCart .map(getAmount) .reduce(sumAmount, 0); } getTotalAmount(shoppingCart); // 120
getAmount 函數接收產品對象并僅返回金額值。 所以我們這里有 [10,30,20,60] 。 然后,通過 reduce 累加所有金額。Nice~
我們看了每個高階函數的工作原理。 我想向您展示一個示例,說明如何在一個簡單的示例中組合所有三個函數。
還是購物車,想象一下在我們的訂單中有一個產品列表:
let shoppingCart = [ { productTitle: "Functional Programming", type: "books", amount: 10 }, { productTitle: "Kindle", type: "eletronics", amount: 30 }, { productTitle: "Shoes", type: "fashion", amount: 20 }, { productTitle: "Clean Code", type: "books", amount: 60 } ]
我們想要購物車中所有圖書的總金額。 就那么簡單, 需要怎樣編寫算法?
使用 filter 函數過濾書籍類型
使用 map 函數將購物車轉換為數量的集合
使用 reduce 函數累加所有項目
let shoppingCart = [ { productTitle: "Functional Programming", type: "books", amount: 10 }, { productTitle: "Kindle", type: "eletronics", amount: 30 }, { productTitle: "Shoes", type: "fashion", amount: 20 }, { productTitle: "Clean Code", type: "books", amount: 60 } ] const byBooks = (order) => order.type == "books"; const getAmount = (order) => order.amount; const sumAmount = (acc, amount) => acc + amount; function getTotalAmount(shoppingCart) { return shoppingCart .filter(byBooks) .map(getAmount) .reduce(sumAmount, 0); } getTotalAmount(shoppingCart); // 70
Done!
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/104050.html
摘要:關鍵字會實例化一個新的對象實例,并在執行構造函數時將指向該實例。原文鏈接譯是什么對象的內部工作原理 原文鏈接:What is this? The Inner Workings of JavaScript Objects (需要梯子) 原文作者:Eric Elliott 譯文永久鏈接:【譯】什么是 this?JavaScript 對象的內部工作原理 譯者:士心 翻譯目的:函數動...
摘要:高階組件和高階函數高階組件是編程中的常見模式。您可以將上面例子中的高階函數用函數的方式來重新整理如你所見,這就像是一個高階函數,這個函數接受一個函數,返回一個新的元素。函數式編程范例旨在避免在應用程序中使用狀態。 原文:The functional side of React作者:Andrea Chiarelli譯者:博軒 React 是現在最流行的 JavaScript 庫之一。使用...
摘要:正在失業中的課多周刊第期我們的微信公眾號,更多精彩內容皆在微信公眾號,歡迎關注。若有幫助,請把課多周刊推薦給你的朋友,你的支持是我們最大的動力。是一種禍害譯本文淺談了在中關于的不好之處。淺談超時一運維的排查方式。 正在失業中的《課多周刊》(第3期) 我們的微信公眾號:fed-talk,更多精彩內容皆在微信公眾號,歡迎關注。 若有幫助,請把 課多周刊 推薦給你的朋友,你的支持是我們最大的...
摘要:正在失業中的課多周刊第期我們的微信公眾號,更多精彩內容皆在微信公眾號,歡迎關注。若有幫助,請把課多周刊推薦給你的朋友,你的支持是我們最大的動力。是一種禍害譯本文淺談了在中關于的不好之處。淺談超時一運維的排查方式。 正在失業中的《課多周刊》(第3期) 我們的微信公眾號:fed-talk,更多精彩內容皆在微信公眾號,歡迎關注。 若有幫助,請把 課多周刊 推薦給你的朋友,你的支持是我們最大的...
閱讀 1093·2021-10-12 10:11
閱讀 877·2019-08-30 15:53
閱讀 2286·2019-08-30 14:15
閱讀 2961·2019-08-30 14:09
閱讀 1197·2019-08-29 17:24
閱讀 972·2019-08-26 18:27
閱讀 1283·2019-08-26 11:57
閱讀 2146·2019-08-23 18:23