原載於 CSS-Tricks 網站文章《Level up your .filter game》。javascript
.filter
是數組內置的迭代方法,它接收一個斷言函數,這個函數會在迭代的每一個數組成員上調用,若是函數的返回值是真值,就過濾出(即保留)這個成員,不然(是假值的話)就過濾掉這個成員。最終 .filter
返回的是原數組的一個子集。css
這一段話裏面有不少概念須要解釋!讓咱們逐一看看。java
「內置」就是表示是語言的一部分——你不須要添加任何庫,就可使用這個函數。數組
「迭代方法」就是一個函數,會在迭代的每一個數組成員上使用。其餘的迭代方法還包括 .map
和 .reduce
。app
「斷言」是指一個返回布爾值的函數。函數
「真值」就是一個值,在轉換成布爾值以後結果爲 true
。幾乎全部的值都是真值,除了 undefined
、null
、false
、0
、NaN
和 ""
(空字符串)。網站
下面開始 .filter
實戰,首先咱們有一個數組變量,裏面是飯店列表。ui
const restaurants = [
{
name: "Dan's Hamburgers",
price: 'Cheap',
cuisine: 'Burger',
},
{
name: "Austin's Pizza",
price: 'Cheap',
cuisine: 'Pizza',
},
{
name: "Via 313",
price: 'Moderate',
cuisine: 'Pizza',
},
{
name: "Bufalina",
price: 'Expensive',
cuisine: 'Pizza',
},
{
name: "P. Terry's",
price: 'Cheap',
cuisine: 'Burger',
},
{
name: "Hopdoddy",
price: 'Expensive',
cuisine: 'Burger',
},
{
name: "Whataburger",
price: 'Moderate',
cuisine: 'Burger',
},
{
name: "Chuy's",
cuisine: 'Tex-Mex',
price: 'Moderate',
},
{
name: "Taquerias Arandina",
cuisine: 'Tex-Mex',
price: 'Cheap',
},
{
name: "El Alma",
cuisine: 'Tex-Mex',
price: 'Expensive',
},
{
name: "Maudie's",
cuisine: 'Tex-Mex',
price: 'Moderate',
},
];
複製代碼
這裏包含許多信息,如今我想吃漢堡,讓咱們把它從這個數組裏過濾出來。this
const isBurger = ({cuisine}) => cuisine === 'Burger';
const burgerJoints = restaurants.filter(isBurger);
複製代碼
isBurger
就是我們的斷言函數了,burgerJoints
是由 restaurants
得來的、新的子集數組。這裏須要注意的是執行 .filter
方法, restaurants
數組自己並不會改變。spa
下面這個 Codepen 筆記裏,burgerJoints
就是過濾以後獲得的數組 (點擊查看):
每個斷言,都有一個對應的否認斷言。
斷言是返回布爾值的函數。由於只有兩個可能的布爾值,這意味着很容易「翻轉」斷言的值。
幾個小時過去了,我餓了,我已經吃過漢堡了,如今想吃點別的,只要不是漢堡就行。一個選擇就是從頭編寫一個 isNotBurger
斷言。
const isBurger = ({cuisine}) => cuisine === 'Burger';
const isNotBurger = ({cuisine}) => cuisine !== 'Burger';
複製代碼
但這看起來好傻啊,兩個斷言太像了,咱們寫了重複代碼,不夠 DRY。另外一種方式是調用以前的 isBurger
斷言,將結果直接取反就好了。
const isBurger = ({cuisine}) => cuisine === 'Burger';
const isNotBurger = restaurant => !isBurger(restaurant);
複製代碼
這個更好! 若是漢堡的定義發生變化,您只需在一個地方更改邏輯。 可是,若是咱們須要同時獲得好幾個想要否認的斷言呢? 因爲這多是常常要作的事情,所以能夠編寫個更通用的 negate
函數。
const negate = predicate => function () {
return !predicate.apply(null, arguments);
}
const isBurger = ({cuisine}) => cuisine === 'Burger';
const isNotBurger = negate(isBurger);
const isPizza = ({cuisine}) => cuisine === 'Pizza';
const isNotPizza = negate(isPizza);
複製代碼
如今,你腦殼裏可能會有些疑問了:
MDN:
apply()
使用給定的this
值和數組(或類數組對象)參數arguments
來調用函數。
MDN:
arguments
是全部函數都(除了箭頭函數)提供的局部變量。在函數內部可使用arguments
對象來引用調用函數時,傳給函數的參數列表。
在這種狀況下,返回傳統函數 function
是必要的,由於參數對象 arguments
_只_在傳統函數中可用。
固然,也能夠這樣搞(將返回函數寫成箭頭函數形式,用剩餘參數運算符來接收參數)。
const negate = predicate => (...args) => !predicate(...args)
複製代碼
正如咱們在 negate
函數中看到的那樣,一個函數很容易在 JavaScript 中返回一個新函數。這對於編寫「斷言建立器」很是有用。咱們回顧一下 isBurger
和 isPizza
斷言。
const isBurger = ({cuisine}) => cuisine === 'Burger';
const isPizza = ({cuisine}) => cuisine === 'Pizza';
複製代碼
這兩個斷言不是互爲否認的,而是具備相同的判斷邏輯,不一樣的僅是在比較的值上。因此咱們能夠把這兩個函數合成一個 isCuisine
函數:
const isCuisine = comparision => ({cuisine}) => cuisine === comparision;
const isBurger = isCuisine('Burger');
const isPizza = isCuisine('Pizza');
複製代碼
這很好!如今,若是咱們須要過濾價格呢?
const isPrice = comparision => ({price}) => price === comparision;
const isCheap = isPrice('Cheap');
const isExpensive = isPrice('Expensive');
複製代碼
如今 isCheap
和 isExpensive
是 DRY 的,isPazza
和 isBurger
也是 DRY 的——可是 isPrice
和 isCuisine
有重複的邏輯代碼! 幸運的是,咱們還能夠進一步抽象。
const isKeyEqualToValue = key => value => object => object[key] === value;
// 這些能夠重寫
const isCuisine = isKeyEqualToValue('cuisine');
const isPrice = isKeyEqualToValue('price');
// 這些不須要改變了
const isBurger = isCuisine('Burger');
const isPizza = isCuisine('Pizza');
const isCheap = isPrice('Cheap');
const isExpensive = isPrice('Expensive');
複製代碼
對我來講,這就是箭頭函數的美妙之處。在一行中,你能夠優雅地建立一個三階函數。isKeyEqualToValue
是能返回 isPrice
的函數,同時它又是能返回 isCheap
的函數。
看,從原來的 restaurants
數組中建立多個過濾列表是多麼容易。
如今咱們能過濾出有漢堡賣或者價格便宜的飯店, 可是若是想過濾出有便宜價格的漢堡飯店呢?一種選擇是將兩個 .filter
放在一塊兒。
const cheapBurgers = restaurants.filter(isCheap).filter(isBurger);
複製代碼
還有一種是將兩個斷言「組合」成一個:
const isCheapBurger = restaurant => isCheap(restaurant) && isBurger(restaurant);
const isCheapPizza = restaurant => isCheap(restaurant) && isPizza(restaurant);
複製代碼
看看全部這些重複的代碼。咱們能夠把它包裝成一個新的函數!
const both = (predicate1, predicate2) => value => (predicate1(value) && predicate2(value);
const isCheapBurger = both(isCheap, isBurger);
const isCheapPizza = both(isCheap, isPizza);
const cheapBurgers = restaurants.filter(isCheapBurger);
const cheapPizza = restautants.filter(isCheapPizza);
複製代碼
若是你想要披薩或漢堡都 OK 怎麼辦?
const both = (predicate1, predicate2) => value => (predicate1(value) || predicate2(value);
const isDelicious = either(isBurger, isPizza);
const deliciousFood = restaurants.filter(isDelicious);
複製代碼
這是朝着正確方向邁出的一步,但若是你有超過兩種你想要包括的食物呢?這不是一個可伸縮的方法。有兩個內置的數組方法 .every
和 .some
在這裏很適合使用,他們都是接受斷言函數的。.every
檢查是否_每一個_成員都能經過斷言,而 .some
則檢查是否有_有_數組成員能經過斷言。
const isDelicious = restaurant => [isPizza, isBurger, isBbq].some(predicate => predicate(restaurant));
const isCheapAndDelicious = restaurant => [isDelicious, isCheap].every(predicate => predicate(restaurant));
複製代碼
並且,和往常同樣,讓咱們把它們封裝到一些有用的抽象中。
const isEvery = predicates => value => predicates.every(predicate => predicate(value));
const isAny = predicates => value => predicates.some(predicate => predicate(value));
const isDelicious = isAny([isBurger, isPizza, isBbq]);
const isCheapAndDelicious = isEvery([isCheap, isDelicious]);
複製代碼
isEvery
和 isAny
兩個函數都接受一個斷言數組,並返回一個斷言函數。
因爲全部這些斷言都很容易由較高階函數建立,所以根據用戶的交互建立和應用這些斷言並不困難。從咱們學到的全部經驗來看,這是一個應用程序的例子,它能夠根據按鈕點擊來搜索餐館。
過濾器是 JavaScript 開發中的重要組成部分。不管是從 API 響應中找出數據,仍是爲了響應用戶交互,都有不少次須要按條件得到一個數組子集的需求。我但願這篇文章可以幫助您理解 .filter
函數和使用斷言,從而編寫更可讀和可維護的代碼。
(完)