JavaScript开发者的10个重要Lodash函数
对于JavaScript开发人员来说,Lodash无需介绍。然而,该库庞大而且常常让人感到压倒性。现在不会了!
Lodash,Lodash,Lodash…我应该从哪里开始!🤔
有一段时间,JavaScript生态系统还很初级;可以将其与西部荒野或丛林相比,许多事情正在发生,但对于日常开发人员的困扰和生产力,答案很少。
然后进入场景,感觉自己像是一场淹没了一切的洪水。从简单的日常需求,如排序到复杂的数据结构转换,Lodash装载着(甚至超载着!)功能,使JS开发人员的生活变得幸福。
Lodash今天在哪里?嗯,它仍然拥有最初提供的所有好东西,甚至更多,但似乎在JavaScript社区中失去了关注度。为什么?我可以想到几个原因:
- Lodash库中的某些函数(现在仍然如此)在应用于大型列表时速度较慢。虽然这从未影响到95%的项目,但剩下的5%的有影响力的开发人员给Lodash带来了负面评价,并且这种效应不断向下传播到基层。
- JS生态系统中存在一种趋势(甚至可以说Go语言的人同样如此),其中傲慢比必要更常见。因此,依赖像Lodash这样的东西被视为愚蠢的,在诸如StackOverflow的论坛上被打压当人们提出这样的解决方案时(“什么?!为这样的事情使用整个库?我可以结合
filter()
和reduce()
在一个简单的函数中实现相同的功能!”)。 - Lodash已经存在很久了。至少按照JS标准来说是这样。它在2012年发布,所以在编写本文时已经过去了将近十年。Lodash的API已经稳定下来,每年没有太多令人兴奋的新功能可以添加(因为根本没有必要),这对于过于兴奋的JS开发人员来说产生了厌倦。
在我看来,不使用Lodash对我们的代码库来说是一个重大损失。它已经为我们在工作中遇到的日常问题提供了经过验证的无错和优雅的解决方案,使用它只会使我们的代码更可读和可维护。
话虽如此,让我们深入了解一些常见(或不常见)的Lodash函数,看看这个库是多么令人难以置信地有帮助和美妙。
克隆…深层次地!
由于JavaScript中的对象是按引用传递的,当想要克隆某个对象并希望新的数据集是不同的时,这给开发人员带来了一个头痛的问题。
let people = [
{
name: 'Arnold',
specialization: 'C++',
},
{
name: 'Phil',
specialization: 'Python',
},
{
name: 'Percy',
specialization: 'JS',
},
];
// 找出写C++的人
let folksDoingCpp = people.filter((person) => person.specialization == 'C++');
// 把他们转为JS!
for (person of folksDoingCpp) {
person.specialization = 'JS';
}
console.log(folksDoingCpp);
// [ { name: 'Arnold', specialization: 'JS' } ]
console.log(people);
/*
[
{ name: 'Arnold', specialization: 'JS' },
{ name: 'Phil', specialization: 'Python' },
{ name: 'Percy', specialization: 'JS' }
]
*/
请注意,尽管我们纯真无辜并充满善意,但原始的people
数组在此过程中发生了变异(Arnold的专业从C++
变为JS
)- 对底层软件系统的完整性造成了重大打击!确实,我们需要一种方法来创建原始数组的真正(深层次)副本。
你可以争辩说这是一种“愚蠢”的JS编码方式;然而,现实情况有些复杂。是的,我们可以使用可爱的解构运算符,但是任何尝试解构复杂对象和数组的人都知道其中的痛苦。然后,还有使用序列化和反序列化(也许是JSON)来实现深度复制的想法,但这只会让你的代码对阅读者更加混乱。
相比之下,当使用Lodash时,解决方案是多么优雅和简洁:
const _ = require('lodash');
let people = [
{
name: 'Arnold',
specialization: 'C++',
},
{
name: 'Phil',
specialization: 'Python',
},
{
name: 'Percy',
specialization: 'JS',
},
];
let peopleCopy = _.cloneDeep(people);
// 找到使用C++编写的人
let folksDoingCpp = peopleCopy.filter(
(person) => person.specialization == 'C++'
);
// 将他们转换为JS!
for (person of folksDoingCpp) {
person.specialization = 'JS';
}
console.log(folksDoingCpp);
// [ { name: 'Arnold', specialization: 'JS' } ]
console.log(people);
/*
[
{ name: 'Arnold', specialization: 'C++' },
{ name: 'Phil', specialization: 'Python' },
{ name: 'Percy', specialization: 'JS' }
]
*/
请注意,在深度克隆后,people
数组保持不变(在这种情况下,Arnold仍然专注于C++)。但更重要的是,这段代码很容易理解。
从数组中删除重复项
从数组中删除重复项听起来像是一个很好的面试/白板问题(记住,怀疑的时候,就把哈希映射器放在问题上!)。当然,你总是可以编写一个自定义函数来做到这一点,但是如果你遇到了几种不同的场景,需要使你的数组保持唯一,你可能需要编写一些 other functions(并且可能会遇到一些微妙的错误),或者你可以使用Lodash!
我们第一个示例是非常简单的,但它仍然展示了Lodash带来的速度和可靠性。试想一下,如果你自己编写所有的自定义逻辑来实现这个目标!
const _ = require('lodash');
const userIds = [12, 13, 14, 12, 5, 34, 11, 12];
const uniqueUserIds = _.uniq(userIds);
console.log(uniqueUserIds);
// [ 12, 13, 14, 5, 34, 11 ]
请注意,最终的数组没有排序,当然,在这里这不是什么问题。但是现在,让我们想象一个更复杂的场景:我们有一个从某处获取的用户数组,但我们希望确保它只包含唯一的用户。使用Lodash很容易实现!
const _ = require('lodash');
const users = [
{ id: 10, name: 'Phil', age: 32 },
{ id: 8, name: 'Jason', age: 44 },
{ id: 11, name: 'Rye', age: 28 },
{ id: 10, name: 'Phil', age: 32 },
];
const uniqueUsers = _.uniqBy(users, 'id');
console.log(uniqueUsers);
/*
[
{ id: 10, name: 'Phil', age: 32 },
{ id: 8, name: 'Jason', age: 44 },
{ id: 11, name: 'Rye', age: 28 }
]
*/
在这个例子中,我们使用了uniqBy()
方法告诉Lodash我们希望根据id
属性使对象保持唯一。一行代码表达了可能需要10-20行代码才能实现的功能,并且引入了更多的bug范围!
Lodash还提供了更多关于使事物保持唯一的方法,我鼓励你看看docs。
两个数组的差异
联合、差异等术语听起来像是留在沉闷的高中集合论课上的词汇,但在日常实践中却经常出现。常常有这样一种情况:我们有一个列表,并希望与另一个列表合并,或者想找出与另一个列表相比唯一的元素;对于这些情况,差异函数非常完美。
让我们从一个简单的场景开始体验差异:你收到了系统中所有用户的id列表,以及那些账户处于活动状态的用户列表。你如何找到不活跃的id呢?很简单,对吧?
const _ = require('lodash');
const allUserIds = [1, 3, 4, 2, 10, 22, 11, 8];
const activeUserIds = [1, 4, 22, 11, 8];
const inactiveUserIds = _.difference(allUserIds, activeUserIds);
console.log(inactiveUserIds);
// [ 3, 2, 10 ]
如果在更实际的情况下,你必须使用对象数组而不是普通的基本类型怎么办?好吧,Lodash有一个很好的differenceBy()
方法来解决这个问题!
const allUsers = [
{ id: 1, name: 'Phil' },
{ id: 2, name: 'John' },
{ id: 3, name: 'Rogg' },
];
const activeUsers = [
{ id: 1, name: 'Phil' },
{ id: 2, name: 'John' },
];
const inactiveUsers = _.differenceBy(allUsers, activeUsers, 'id');
console.log(inactiveUsers);
// [ { id: 3, name: 'Rogg' } ]
很不错,对吧?!
与difference类似,Lodash中还有其他常见集合操作的方法:union、intersection等。
扁平化数组
需要对数组进行扁平化的情况经常出现。一个应用场景是你收到了一个API的响应,并且需要在复杂的嵌套对象/数组列表上应用一些map()
和filter()
组合来提取出用户id,现在你只剩下一些数组的数组了。以下是一个描述这种情况的代码片段:
const orderData = {
internal: [
{ userId: 1, date: '2021-09-09', amount: 230.0, type: 'prepaid' },
{ userId: 2, date: '2021-07-07', amount: 130.0, type: 'prepaid' },
],
external: [
{ userId: 3, date: '2021-08-08', amount: 30.0, type: 'postpaid' },
{ userId: 4, date: '2021-06-06', amount: 330.0, type: 'postpaid' },
],
};
// 找出下单了后付费订单(内部或外部)的用户id
const postpaidUserIds = [];
for (const [orderType, orders] of Object.entries(orderData)) {
postpaidUserIds.push(orders.filter((order) => order.type === 'postpaid'));
}
console.log(postpaidUserIds);
你能猜到postPaidUserIds
现在是什么样子吗?提示:很糟糕!
[
[],
[
{ userId: 3, date: '2021-08-08', amount: 30, type: 'postpaid' },
{ userId: 4, date: '2021-06-06', amount: 330, type: 'postpaid' }
]
]
现在,如果你是一个明智的人,你不想编写自定义逻辑来提取订单对象并将它们整齐地放在数组中的一行内。只需使用flatten()
方法,享受葡萄吧:
const flatUserIds = _.flatten(postpaidUserIds);
console.log(flatUserIds);
/*
[
{ userId: 3, date: '2021-08-08', amount: 30, type: 'postpaid' },
{ userId: 4, date: '2021-06-06', amount: 330, type: 'postpaid' }
]
*/
请注意,flatten()
只会扁平化一层。如果你的对象被深度嵌套了两层、三层或更多层,flatten()
会让你失望。在这些情况下,Lodash提供了flattenDeep()
方法,但请注意,在非常大的结构上应用此方法可能会导致性能下降(因为在幕后有一个递归操作)。
对象/数组是否为空?
由于JavaScript中“假值”和类型的工作方式,有时简单的检查对象/数组是否为空也会产生存在主义的恐惧。
你如何检查一个数组是否为空?你可以检查它的length
是否为0
。那么,你如何检查一个对象是否为空呢?唔……等一下!这就是那种令人不安的感觉<link_6>
的地方,那些包含[] == false
和{} == false
的JavaScript示例开始在我们的头脑中盘旋。当我们在压力下要交付一个功能时,这样的地雷是你最不需要的事情——它们会让你的代码难以理解,并且会在你的测试套件中引入不确定性。
处理缺失数据
在现实世界中,数据会听从我们的话;无论我们多么希望它是简单和健全的,但事实很少如此。一个典型的例子是在作为API响应接收到的大型数据结构中缺少空对象/数组。
假设我们收到以下对象作为API响应:
const apiResponse = {
id: 33467,
paymentRefernce: 'AEE3356T68',
// `order` 对象缺失
processedAt: `2021-10-10 00:00:00`,
};
如上所示,我们通常在API响应中获取一个order
对象,但并不总是如此。那么,如果我们有一些依赖于这个对象的代码呢?一种方法是进行防御性编码,但是根据order
对象的嵌套程度,如果我们想避免运行时错误,很快就会写出非常丑陋的代码:
if (
apiResponse.order &&
apiResponse.order.payee &&
apiResponse.order.payee.address
) {
console.log(
'The order was sent to the zip code: ' +
apiResponse.order.payee.address.zipCode
);
}
🤢🤢 是的,写起来非常丑陋,阅读起来也非常丑陋,维护起来也非常丑陋,等等。谢天谢地,Lodash有一种简单的处理这种情况的方法。
const zipCode = _.get(apiResponse, 'order.payee.address.zipCode');
console.log('The order was sent to the zip code: ' + zipCode);
// The order was sent to the zip code: undefined
还有一个很棒的选项,可以提供一个默认值,而不是得到undefined
的缺失内容:
const zipCode2 = _.get(apiResponse, 'order.payee.address.zipCode', 'NA');
console.log('The order was sent to the zip code: ' + zipCode2);
// The order was sent to the zip code: NA
我不知道你是怎么想的,但get()
是那种让我眼泪欢乐的东西之一。它并不起眼。没有需要记忆的特定语法或选项,但看看它可以减轻多少集体的痛苦!😇
防抖
如果你不熟悉,防抖在前端开发中是一个常见的主题。其思想是有时候不立即而是在一段时间后(通常是几毫秒)启动一个操作是有益的。这是什么意思?下面是一个例子。
想象一个电子商务网站,有一个搜索栏(实际上,这些天任何网站/网络应用都有!)。为了更好的用户体验,我们不希望用户必须按回车键(或更糟糕的是,按下“搜索”按钮),才能根据他们的搜索词显示建议/预览。但是显而易见的答案有点负担过重:如果我们为搜索栏添加一个onChange()
事件监听器,并且为每次按键都发起一个API调用,那么我们将为后端创建一个噩梦;将会有太多不必要的调用(例如,如果搜索“白地毯刷”,将会总共有18个请求!),而且几乎所有这些调用都是无关紧要的,因为用户输入还没有完成。
答案在于去抖动,思路是这样的:在文本更改后不要立即发送API调用。等待一段时间(比如200毫秒),如果在那段时间内有其他按键,则取消之前的计时并重新开始等待。因此,只有当用户暂停(因为他们在思考或者他们已经完成并期望得到一些响应)时,我们才会向后端发送API请求。
我描述的整体策略比较复杂,我不会详细介绍计时器管理和取消的同步;然而,如果你使用Lodash,去抖动的实际过程非常简单。
如果你想到了setTimeout()可以完成同样的工作,那么你错了!Lodash的debounce具有许多强大的功能;例如,您可能希望确保去抖动不是无限的。也就是说,即使在函数即将触发时每次都有按键(从而取消整个过程),您可能仍然希望在2秒后发出API调用。为此,Lodash的debounce()有一个maxWait选项:
请查看官方文档以了解更多深入的内容。它们充满了非常重要的东西!
从数组中删除值
我不知道你是怎么看待的,但我讨厌编写从数组中删除项目的代码。首先,我必须获取项目的索引,检查索引是否有效,如果有效,调用splice()方法等等。我永远记不住语法,因此需要一直查找,最后我留下了一种令人讨厌的感觉,即我让一些愚蠢的错误悄悄爬进了代码。
请注意两件事:
原始数组在此过程中发生了变化。
pull()方法删除所有实例,即使存在重复项。
还有另一个相关方法叫做pullAll(),它接受一个数组作为第二个参数,可以更轻松地一次删除多个项目。虽然我们可以使用spread运算符与pull()一起使用,但请记住,Lodash出现在spread运算符还不是语言的提议时期!
元素的最后一个索引
JavaScript的原生indexOf()方法很棒,除非您有兴趣从相反的方向扫描数组!再一次,是的,您可以编写一个递减循环并找到元素,但为什么不使用更优雅的技术呢?
这是一个使用lastIndexOf()方法的快速Lodash解决方案:
不幸的是,这个方法没有变体,我们无法查找复杂对象,甚至传递自定义查找函数。
Zip. Unzip!
除非你在Python中工作过,否则作为JavaScript开发人员,你可能永远不会注意或想象到zip/unzip这个实用程序。也许这是有原因的:很少有像filter()
等那样迫切需要zip/unzip的情况。然而,它是最好的鲜为人知的实用工具之一,在某些情况下可以帮助你创建简洁的代码。
与其听起来像是压缩有关,zip/unzip实际上与压缩无关。相反,它是一种分组操作,其中具有相同长度的数组可以被转换为一个数组的数组,其中位置相同的元素被打包在一起(zip()
)并返回(unzip()
)。是的,我知道,用文字来解释有点模糊,所以让我们看一些代码:
const animals = ['duck', 'sheep'];
const sizes = ['small', 'large'];
const weight = ['less', 'more'];
const groupedAnimals = _.zip(animals, sizes, weight);
console.log(groupedAnimals);
// [ [ 'duck', 'small', 'less' ], [ 'sheep', 'large', 'more' ] ]
原始的三个数组被转换成了只有两个数组的单个数组。而这些新数组中的每一个表示一个动物及其所有信息在一个地方。因此,索引0
告诉我们它是什么类型的动物,索引1
告诉我们它的大小,索引2
告诉我们它的重量。结果,数据现在更容易处理了。一旦你对数据应用了所需的操作,你可以使用unzip()
再次将其分解并发送回原始来源:
const animalData = _.unzip(groupedAnimals);
console.log(animalData);
// [ [ 'duck', 'sheep' ], [ 'small', 'large' ], [ 'less', 'more' ] ]
zip/unzip实用程序不是能够一夜改变你生活的东西,但它将在某一天改变你的生活!
结论 👨🏫
(我在这篇文章中使用的所有源代码都放在here,供你直接从浏览器中尝试!)
Lodash库docs充满了会让你惊叹的示例和函数。在JS生态系统中,自虐行为似乎越来越多的时代,Lodash就像一股清新的空气,我强烈推荐你在项目中使用这个库!