我們著眼於使用map,filter和reduce來操縱對象數組,使用從函數式編程中借用的技術。
數據操作是任何JavaScript應用程式中的常見任務。幸運的是,新的陣列處理運營商map,filter以及reduce廣泛的支持。雖然這些功能的文檔是足夠的,但它通常會顯示非常基本的實現用例。在日常使用中,我們通常需要使用這些方法來處理數據對象的數組,這是文檔中缺乏的場景。另外,這些操作符經常被用功能性語言來看,並為JavaScript帶來了一種新的透視功能,即通過具有功能性觸摸的對象進行迭代。
在這篇文章中,我們將打破使用的幾個例子map,reduce和filter操作對象的數組。通過這些例子,我們將學習這些方法的強大程度,同時了解它們與函數式編程的關係。
功能編程:好的部分
函數式編程有許多概念超出了我們要實現的範圍。在本文中,我們將討論一些絕對的基礎知識。
在整個示例中,我們將傾向於使用多個語句的單個表達式。這意味著我們將大大減少使用的變量數量,並儘可能地利用函數組合和方法連結。這種編程風格減少了維護狀態的需要,這與功能性編程實踐相一致。
使用的一個共同利益map,並filter為每個運營商創建新的數組。通過創建新數組而不是修改或改變現有數據,我們的代碼將更容易推理,因為變異數據可能會導致意想不到的副作用。
最後,我們將利用每個運算符都是更高階函數的事實。簡而言之,每個運營商都接受一個回調函數作為參數。回調函數允許我們通過函數委託來定製行為,這是一個強大的代碼靈活性和可讀性的工具。
這裡的想法並不是「功能齊全」,而是在方便時利用概念。如果這些概念與您聯繫在一起,則可以下載功能性編程中常用的JavaScript方法/函數列表的備忘單。該函數式編程小抄是一個偉大的快速參考,以保持得心應手。
無處不在的箭頭/函數表達式
當來自較舊的JavaScript庫時,「胖箭頭」可能看起來有些陌生。此=>箭頭運算符用於減少編寫函數所需的代碼量。當我們需要一個我們可以使用的簡單函數時,不必用return語句顯式地寫函數()Identifier => Expression。這有助於使我們的代碼更易於閱讀,特別是在使用高階函數時,以函數作為參數的方法。.map,.reduce並且.filter都是更高階的函數。
沒有函數表達式,我們可能會這樣寫:
let cart = [{ name: "Drink", price: 3.12 }, { name: "Steak", price: 45.15}, { name: "Drink", price: 11.01}];let steakOrders = cart.filter(function(obj) { return obj.name === "Steak"});// { name: "Steak", price: 45.15 }
使用箭頭運算符,我們可以考慮編寫相同的代碼,省略如下例所示的函數:
let steakOrders = cart.filter((obj) => { return obj.name === "Steak" });// { name: "Steak", price: 45.15 }
但是,我們可以更進一步,因為大多數表達式在使用箭頭運算符時已經隱含。代碼可以進一步縮小:
et steakOrders = cart.filter(obj => obj.name === "Steak");// { name: "Steak", price: 45.15 }
在這個例子中,我們可以看到箭頭幫助在過濾器語句中定義了一個簡潔的函數。現在我們已經看到了箭頭如何改進表達式的寫法,讓我們進一步了解使用該filter方法。
過濾對象數組
該filter方法創建一個新數組,其中包含所有通過由提供的函數實現的測試的元素。當針對對象使用過濾器方法時,我們可以根據對象上的一個或多個屬性創建一個受限制的集合。
在這個例子中,我們通過傳入一個函數表達式來測試名稱屬性的值,以得到名稱為「Steak」的集合中的對象obj => obj.name === "Steak":
let cart = [{ name: "Drink", price: 3.12 }, { name: "Steak", price: 45.15}, { name: "Drink", price: 11.01}];let steakOrders = cart.filter(obj => obj.name === "Steak");// { name: "Steak", price: 45.15 }
考慮到我們可能需要一部分數據,我們需要在多個屬性值上進行過濾。我們可以通過編寫一個包含多個測試的函數來實現:
let expensiveDrinkOrders =cart.filter(x => x.name === "Drink" && x.price > 10);// { name: "Drink", price: 11.01 }
當對多個值進行過濾時,函數表達式可能會變得冗長,從而使代碼難以一目了然。使用一些技巧,我們可以簡化代碼並使其更易於管理。
解決該問題的一種方法是使用多個過濾器代替&&操作員。通過將多個過濾器連結在一起,我們可以在產生相同結果的同時使聲明更易於推理。
let expensiveDrinkOrders = cart.filter(x => x.name === "Drink").filter(x => x.price > 10);// { name: "Drink", price: 11.01 }
另一種表達複雜過濾器的方法是創建一個命名函數。這將使我們能夠以「人類可讀」的方式編寫過濾器:
const drinksGreaterThan10 =obj => obj.name === "Drink" && obj.price > 10;let result = cart.filter(drinksGreaterThan10);
雖然這確實按預期工作,但價格值是硬編碼的。我們可以通過允許使用參數在運行時設置價格來優化可讀性和靈活性。
起初,將參數添加obj到它讀取的參數可能看起來很直觀,變量價格(obj, cost)在哪裡cost:
const drinksGreaterThan =(obj, cost) => obj.name === "Drink" && obj.price > cost;let result = cart.filter(drinksGreaterThan(10)); // Error
用JavaScript編寫
不幸的是,這會產生一個錯誤,因為過濾器需要一個帶有單個參數的函數,現在我們試圖使用兩個參數。為了正確地滿足參數,我們需要將過濾語句寫為.filter(x => drinksGreaterThan(x, 10))。
為了提供更好的解決方案,我們可以使用稱為currying的函數式編程技術。Currying允許將具有多個參數的函數轉換為函數序列,從而允許我們創建與其他函數籤名的奇偶校驗。
讓我們將cost參數移到一個函數表達式中,並drinksGreaterThan使用currying 重寫該函數。這只是巧妙地使用高階函數,其中drinksGreatThan變成了一個接受成本並返回另一個函數的函數,該函數接受obj:
const drinksGreaterThan =cost => obj => obj.name === "Drink" && obj.price > cost;let result = cart.filter(drinksGreaterThan(10));// { name: "Drink", price: 11.01 }
完成的功能為我們提供了一個具有最大可讀性和可重用性的命名過濾器。
映射對象
該 map 方法是轉換和投影數據的重要工具。該map方法創建一個新的數組,其結果是對調用數組中的每個元素調用一個提供的函數。
在我們使用外部數據源的情況下,我們可能會提供比所需更多的數據。我們可以使用map我們認為合適的方式來投影數據,而不是處理完整的數據集。在以下示例中,我們將收到包含多個屬性的數據,其中大多數屬性在當前視圖中未使用。
初始對象包含: id, name, price, cost,和 size:
let jsonData = [{ id: 1, name: "Soda", price: 3.12, cost: 1.04, size: "4oz", }, { id: 2, name: "Beer", price: 6.50, cost: 2.45, size: "8oz" }, { id: 3, name: "Margarita", price: 12.99, cost: 4.45, size: "12oz" }];
我們認為,我們只會使用名稱和價格屬性,因此我們將使用它 map 來構建新的數據集:
let drinkMenu = jsonData.map(x => ({name: x.name, price: x.price}));// [{"name":"Soda","price":3.12}, {"name":"Beer","price":6.5}, {"name":"Margarita","price":12.99}]
使用時map,我們可以將每個項目的值分配給僅具有名稱和價格屬性的新對象。
如果我們關心map函數表達式的可讀性,我們可以將該函數分配給一個變量。這種抽象並不改變map操作方式,但是,對於那些不熟悉域的人來說,代碼的意圖會變得更加清晰。
通過創建一個名為的獨特方法toMenuItem,我們可以立即給我們的代碼上下文。該 map 聲明變成了自我記錄,因為它可以朗讀,「JSON數據,映射到菜單項」。
const toMenuItem = x => ({name: x.name, price: x.price});let drinkMenu = jsonData.map(toMenuItem);// [{"name":"Soda","price":3.12}, {"name":"Beer","price":6.5}, {"name":"Margarita","price":12.99}]
繼續這個例子,我們將map再次使用來應用一個值轉換。假設我們想將現有的菜單價格從美元轉換為歐元。由於 map 不會改變初始數組,所以我們不必擔心drinkMenu由於我們的轉換而導致實例更改的狀態。
讓我們使用一個類似的函數來轉換價格值,除了這次我們將保留對象可用的所有屬性。為了確保我們複製每個值,我們將使用...spread運算符:
const drinkMenuEuro = drinkMenu.map(x => ({...x, price: (x.price * 0.81).toFixed(2)})// [{"name":"Soda","price":"2.53"},{"name":"Beer","price":"5.27"},{"name":"Margarita","price":"10.52"}]
在這個例子中,...x(擴展運算符)為我們做了很多。這一小段代碼可以確保我們複製整個對象及其屬性,同時只修改價格值。使用spread運算符的好處是我們可以稍後添加其他屬性,drinkMenu並且它們將被drinkMenuEuro自動包含 。
減少與對象
Reduce是一個使用不充分,有時會被誤解的數組運算符。該reduce方法對累加器和數組中的每個元素(從左到右)應用一個函數,以將其減少為單個值。
當使用reduce對象的數組時,累加器值表示前一次迭代產生的對象。如果我們需要獲取對象屬性的總值(即obj.price),我們首先需要將該屬性映射到單個值:
let cart = [{ name: "Soda", price: 3.12 }, { name: "Margarita", price: 12.99}, { name: "Beer", price: 6.50}];let totalPrice = cart.reduce((acc,next) => acc.price + next.price);// NaN because acc cannot be both an object and numberlet totalPrice = cart.map(obj => obj.price).reduce((acc, next) => acc + next);// 22.61
在前面的例子中,我們看到當我們減少一個對象數組時,累加器就是對象。該reduce方法對於正確條件下的對象非常有用。我們不用試圖總結一個對象的價格,而是使用reduce來查找價格最高的對象:
let mostExpensiveItem = cart.reduce((acc, next) => acc.price > next.price ? acc : next);// { name: "Margarita", price: 12.99}
在這裡,我們利用功能 reduce 來給我們最大的目標。
我們可以使用函數委託來描述我們的意圖。這將有助于澄清reduce操作符中的表達式。我們最終的功能是「以最大的價格降低」:
let byGreatestPrice = (item, next) => item.price > next.price ? item : next;let mostExpensiveItem = cart.reduce(byGreatestPrice);// { name: "Margarita", price: 12.99}
結論
在這篇文章中,我們討論了使用map,reduce以及filter對操作對象的數組。由於這些數組運算符不會修改調用數組的狀態,所以我們可以有效地使用它們而不用擔心副作用。使用從函數式編程中借鑑的技巧,我們可以編寫出功能強大的表達式數組運算符 通過函數委託,我們可以選擇顯式函數名來創建可讀代碼。