[译]玩转 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 函数和使用断言,从而编写更可读和可维护的代码。

(完)

相关文章
相关标签/搜索