JavaScript 开发者的 10 个重要 Lodash 函数

深入解析 Lodash:JavaScript 开发者的必备利器

对于 JavaScript 开发者而言,Lodash 库几乎是无人不知的。 然而,其庞大的体量和众多的功能常常让人感到无从下手。 但是,现在不必再为此烦恼了!

Lodash,Lodash,Lodash… 这究竟应该从何谈起呢? 🤔

回想早期的 JavaScript 生态系统,它就像一个刚刚起步的蛮荒之地。各种新奇事物层出不穷,但对于日常开发人员来说,却缺乏切实可行的解决方案,这往往导致挫败感和生产力低下。

后来,Lodash 的出现,如同久旱逢甘霖。 从简单的排序到复杂的数据结构转换,Lodash 提供了丰富的功能,极大地提升了 JavaScript 开发者的幸福感。

Lodash,你好!

如今的 Lodash 发展如何呢? 它依然保留着最初的所有优点,甚至更多。 但是,它在 JavaScript 社区中的份额似乎有所下降。 这是为什么呢? 我认为有以下几个原因:

  • Lodash 库中的某些函数在处理大型列表时效率较低,这一点在今天依然存在。 虽然这可能不会影响 95% 的项目,但来自剩余 5% 的有影响力的开发人员的负面评价,给 Lodash 带来了不好的名声,并且这种影响也逐渐传递到了基层。
  • 在 JavaScript 生态系统中,有一种倾向(甚至可能 Golang 开发者也认同),即自以为是比实际需求更为普遍。 因此,依赖像 Lodash 这样的库被认为是愚蠢的。当人们提出使用 Lodash 的解决方案时,往往会在 StackOverflow 等论坛上遭到拒绝(例如,“什么?!用整个库来做这种事?我可以用 filter() 和 reduce() 组合在一个简单的函数中完成同样的事情!”)。
  • Lodash 已经诞生很长时间了。 至少按照 JS 标准来看是这样。 它于 2012 年问世,到本文撰写时,已经快十年了。 它的 API 一直很稳定,每年都没有太多令人兴奋的新增功能(仅仅是因为没有必要),这让那些普遍过度兴奋的 JS 开发者感到厌倦。

在我看来,不使用 Lodash 对我们的 JavaScript 代码库来说是一个巨大的损失。 对于我们在工作中遇到的日常问题,它已被证明是可靠且优雅的解决方案。 使用它只会使我们的代码更具可读性和可维护性。

话不多说,让我们深入研究一些常用的(或不常用的)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++)。 更重要的是,代码简单易懂。

从数组中删除重复项

从数组中删除重复项听起来像是面试或白板上的常见问题(记住,如果不知道该怎么做,就直接用哈希表!)。 当然,你总是可以编写一个自定义函数来实现这个目标。 但是,如果你遇到几种不同的情况,需要使你的数组保持唯一,该怎么办呢? 你可以为此编写几个不同的函数(并承担遇到细微错误的风险),或者你可以直接使用 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 行代码,并引入更多错误的可能性!

关于如何在 Lodash 中实现去重,还有很多可用的方法,我鼓励你查看 官方文档

比较两个数组的差异

并集、差集等等,这些术语听起来像是枯燥的高中集合论课程中的内容,但它们在日常实践中经常出现。 拥有一个列表,并希望将其与另一个列表合并,或者想要找到相对于另一个列表哪些元素是独有的,这些都是很常见的需求。 对于这些场景,`difference` 函数非常适合。

你好,A。再见,B!

让我们从一个简单的差异场景开始:你收到一个系统中所有用户 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 中“虚值”和类型的工作方式,有时检查是否为空这样简单的事情,会导致焦虑和恐惧。

如何检查一个数组是否为空? 你可以检查它的长度是否为 0。 现在,如何检查一个对象是否为空? 嗯… 等等! 这正是那种不安的感觉开始出现的地方,那些诸如 `[] == false` 和 `{}` == false 的想法开始盘旋在我们的脑海中。 当面临交付功能的压力时,像这样的雷区是你最不需要的。 它们会使你的代码难以理解,并会在你的测试套件中引入不确定性。

处理缺失的数据

在现实世界中,数据并不总是按照我们希望的方式呈现。无论我们多么渴望,它都很少是简洁和理智的。 一个典型的例子是,在作为 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

还有一个很棒的选项,可以提供默认值,而不是因为缺少某些东西而变得不确定:

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,实际的防抖过程非常简单。

const _ = require('lodash');
  const axios = require('axios');
  
  // 顺便说一句,这是一个真正的狗狗 API!
  const fetchDogBreeds = () =>
    axios
      .get('https://dog.ceo/api/breeds/list/all')
      .then((res) => console.log(res.data));
  
  const debouncedFetchDogBreeds = _.debounce(fetchDogBreeds, 1000); // 一秒后
  debouncedFetchDogBreeds(); // 一段时间后显示数据
  

如果你正在考虑 `setTimeout()` 也能做同样的工作,那么,还有更多! Lodash 的 `debounce` 具有许多强大的功能。例如,你可能希望确保防抖不是无限期的。 也就是说,即使每次函数即将启动时都有一次击键(从而取消了整个过程),你可能希望确保 API 调用无论如何都会在两秒后进行。 为此,Lodash 的 `debounce()` 具有 `maxWait` 选项:

const debouncedFetchDogBreeds = _.debounce(fetchDogBreeds, 150, { maxWait: 2000 }); // 防抖 150 毫秒,但无论如何,在 2 秒后发送 API 请求

请查看官方 文档 进行更深入的研究。 它们充满了超级重要的信息!

从数组中删除值

我不了解你,但我讨厌编写从数组中删除项目的代码。 首先,我必须获取项目的索引,检查索引是否有效,如果有效,则调用 `splice()` 方法等等。 我永远记不住语法,因此需要一直查找东西。最后,我一直觉得我已经让一些愚蠢的错误悄悄潜入了。

const greetings = ['hello', 'hi', 'hey', 'wave', 'hi'];
  _.pull(greetings, 'wave', 'hi');
  console.log(greetings);
  // [ 'hello', 'hey' ]

请注意两件事:

  • 原始数组在此过程中发生了改变。
  • `pull()` 方法会删除所有实例,即使存在重复项也是如此。

还有一个名为 `pullAll()` 的相关方法,它接受一个数组作为第二个参数,从而更容易一次删除多个项目。 当然,我们可以将 `pull()` 与展开运算符一起使用,但请记住,Lodash 出现的时候展开运算符甚至还不是该语言的提案!

const greetings2 = ['hello', 'hi', 'hey', 'wave', 'hi'];
  _.pullAll(greetings2, ['wave', 'hi']);
  console.log(greetings2);
  // [ 'hello', 'hey' ]

元素的最后一个索引

JavaScript 的原生 `indexOf()` 方法很棒,除非你想要从相反的方向扫描数组! 再一次,是的,你可以编写一个递减循环来查找元素,但为什么不使用更优雅的技术呢?

这是一个使用 `lastIndexOf()` 方法的快速 Lodash 解决方案:

const integers = [2, 4, 1, 6, -1, 10, 3, -1, 7];
  const index = _.lastIndexOf(integers, -1);
  console.log(index); // 7

遗憾的是,没有这种方法的变体,我们可以用它来查找复杂的对象,甚至是传递自定义的查找函数。

压缩与解压缩

除非你使用过 Python,否则 `zip/unzip` 可能是在你作为 JavaScript 开发者的整个职业生涯中永远不会注意到或想象的实用工具。 也许有一个很好的理由:很少有像 `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` 实用工具不会在一夜之间改变你的生活,但总有一天会派上用场!

总结👨‍🏫

(我把本文用到的所有源码放在 这里,供你直接从浏览器尝试!)

Lodash 文档 充满了让你大吃一惊的例子和函数。 在这个 JS 生态系统似乎越来越混乱的时代,Lodash 就像一股清新的空气。我强烈建议你在你的项目中使用这个库!