[譯]玩轉 JS 數組的 .filter 方法

原載於 CSS-Tricks 網站文章《Level up your .filter game》javascript

.filter 是數組內置的迭代方法,它接收一個斷言函數,這個函數會在迭代的每一個數組成員上調用,若是函數的返回值是真值,就過濾出(即保留)這個成員,不然(是假值的話)就過濾掉這個成員。最終 .filter 返回的是原數組的一個子集。css

這一段話裏面有不少概念須要解釋!讓咱們逐一看看。java

  • 「內置」就是表示是語言的一部分——你不須要添加任何庫,就可使用這個函數。數組

  • 「迭代方法」就是一個函數,會在迭代的每一個數組成員上使用。其餘的迭代方法還包括 .map.reduceapp

  • 「斷言」是指一個返回布爾值的函數。函數

  • 「真值」就是一個值,在轉換成布爾值以後結果爲 true。幾乎全部的值都是真值,除了 undefinednullfalse0NaN""(空字符串)。網站

下面開始 .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);
複製代碼

如今,你腦殼裏可能會有些疑問了:

.apply 是啥?

MDN

apply() 使用給定的 this 值和數組(或類數組對象)參數 arguments 來調用函數。

arguments 是什麼?

MDN

arguments 是全部函數都(除了箭頭函數)提供的局部變量。在函數內部可使用 arguments 對象來引用調用函數時,傳給函數的參數列表。

爲何用老的 function 形式,而不是新的更酷的箭頭函數?

在這種狀況下,返回傳統函數 function 是必要的,由於參數對象 arguments _只_在傳統函數中可用。

固然,也能夠這樣搞(將返回函數寫成箭頭函數形式,用剩餘參數運算符來接收參數)。

const negate = predicate => (...args) => !predicate(...args)
複製代碼

返回斷言

正如咱們在 negate 函數中看到的那樣,一個函數很容易在 JavaScript 中返回一個新函數。這對於編寫「斷言建立器」很是有用。咱們回顧一下 isBurgerisPizza 斷言。

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');
複製代碼

如今 isCheapisExpensive 是 DRY 的,isPazzaisBurger 也是 DRY 的——可是 isPriceisCuisine 有重複的邏輯代碼! 幸運的是,咱們還能夠進一步抽象。

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]);
複製代碼

isEveryisAny 兩個函數都接受一個斷言數組,並返回一個斷言函數。

因爲全部這些斷言都很容易由較高階函數建立,所以根據用戶的交互建立和應用這些斷言並不困難。從咱們學到的全部經驗來看,這是一個應用程序的例子,它能夠根據按鈕點擊來搜索餐館。

總結

過濾器是 JavaScript 開發中的重要組成部分。不管是從 API 響應中找出數據,仍是爲了響應用戶交互,都有不少次須要按條件得到一個數組子集的需求。我但願這篇文章可以幫助您理解 .filter 函數和使用斷言,從而編寫更可讀和可維護的代碼。

(完)

相關文章
相關標籤/搜索