原载于 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
函数和使用断言,从而编写更可读和可维护的代码。
(完)