.filter 是数组内置的迭代方法,它接收一个断言函数,这个函数会在迭代的每个数组成员上调用,如果函数的返回值是真值,就过滤出(即保留)这个成员,否则(是假值的话)就过滤掉这个成员。最终 .filter 返回的是原数组的一个子集。
这一段话里面有很多概念需要解释!让我们逐一看看。
“内置”就是表示是语言的一部分——你不需要添加任何库,就可以使用这个函数。
“迭代方法”就是一个函数,会在迭代的每个数组成员上使用。其他的迭代方法还包括 .map 和 .reduce。
“断言”是指一个返回布尔值的函数。
“真值”就是一个值,在转换成布尔值之后结果为 true。几乎所有的值都是真值,除了 undefined、null、false、0、NaN 和 ""(空字符串)。
下面开始 .filter 实战,首先我们有一个数组变量,里面是饭店列表。
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',
},
];
复制代码
这里包含许多信息,现在我想吃汉堡,让我们把它从这个数组里过滤出来。
const isBurger = ({cuisine}) => cuisine === 'Burger';
const burgerJoints = restaurants.filter(isBurger);
复制代码
isBurger 就是咱们的断言函数了,burgerJoints 是由 restaurants 得来的、新的子集数组。这里需要注意的是执行 .filter 方法, restaurants 数组本身并不会改变。
下面这个 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 是啥?
apply() 使用给定的 this 值和数组(或类数组对象)参数 arguments 来调用函数。
arguments 是什么?
arguments 是所有函数都(除了箭头函数)提供的局部变量。在函数内部可以使用 arguments 对象来引用调用函数时,传给函数的参数列表。
为什么用老的 function 形式,而不是新的更酷的箭头函数?
在这种情况下,返回传统函数 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 函数和使用断言,从而编写更可读和可维护的代码。
(完)