收藏文章 楼主

13 个JavaScript 数组小技巧,让你更像技术专家

版块:网站建设   类型:普通   作者:小羊羔links   查看:495   回复:0   获赞:0   时间:2022-01-23 23:09:58

来源 | https://javascript.plainenglish.io/13-neat-tricks-to-manipulate-arrays-like-an-expert-b0bb19f0c936

翻译 | 杨小爱


我们每天都在处理集合,通常是处理从 API 提供的元素列表。但是,这些 API 可能不会返回我们想要的结果 我们感兴趣的形状。
因此,在客户端 JavaScript 中操作数组是很常见的。在解决这些问题的所有不同方法中,我们将使用最好的方法 函数式编程函数。
我们不会在本文中介绍函数式编程的基础知识,因此,您可能需要阅读本文以外的一些知识,以获取有关此主题的入门知识。
我们的工作将尊重以下假设
简洁性 我们不会改变任何对象,并且总是会返回新对象。
不变性 我们的方法不会改变数组的状态,而是返回一个与我们的规范相对应的新状态。
以下是我在撰写本文时遵循的一些约定
有些观点会迅速谈论可以完成相同工作的另一种方式,以及未使用它的原因。
当我觉得它可以带来一些有价值的东西时,我会提供一些奖励积分。
有些点是可选的,但我相信值得一提。
准备好了吗?我们现在开始吧!
1、在边缘添加一个元素
让我们从简单的开始。它利用扩展语法来精美地传达其在末尾附加元素的意图。
const elements = [1234];const appendedElements = [...elements, 5]; // [1, 2, 3, 4, 5]const prependedElements = [0, ...appendedElements]; // [0, 1, 2, 3, 4, 5]

为什么我们不使用push?

因为 push 是 Array 对象的就地方法之一。它改变了现有的对象,这违反了我们的不变性原则。

2、从边缘移除一个元素

要从边缘移除一个元素,无论是第一个元素还是最后一个元素,我们将使用 slice 方法。

const elements = [1, 2, 3, 4, 5];
// remove last elementconst lastElementRemoved = elements.slice(0, 4); / [1, 2, 3, 4]
// remove first elementconst firstElementRemoved = elements.slice(1); / [2, 3, 4, 5]
// remove first and last element (chaining)const firstAndLastElementRemoved = elements.slice(0, 4).slice(1) / [2, 3, 4]

为什么不使用splice?

出于同样的原因,我们不使用push。由于这两种方法的名称相似,因此将它们混合在代码中可能会造成混淆。我们不使用splice,也是一样的,在这里,我建议只使用 slice,除非性能很重要。

加餐 删除任意位置的元素

我们还可以利用我们对 slice 方法的知识来删除任意位置的元素。

const elements = [1, 2, 3, 4, 5];const indexToRemove = 2; // starts from 0, so it targets the third elementconst nextElements = [  ...elements.slice(0, indexToRemove),   ...elements.slice(indexToRemove + 1)]; // [1, 2, 4, 5]

但这不是最好的方法,原因如下

  • 它调用 slice 两次,创建两个数组。

  • 它将结果合并到第三个数组中,总共创建了三个对象。

代码的意图尚不清楚,可能很难理解我们正在从任意点删除元素(因此,使用额外的变量 indexToRemove 来提高清晰度 。

稍后我们将看到使用过滤器的更好方法。

3、更新数组的所有元素

我们现在将使用更传统的函数式编程原语,这一次使用经典的map。

const users = [  {    name: "Jane",    balance: 100.00  },  {    name: "John",    balance: 55.25  }];
// define pure methods for our `map`const double = (amount) => amount * 2;const doubleUserBalance = (user) => ({ ...user, balance: double(user.balance),});
// transform all the usersconst usersWithDoubledBalance = users.map(doubleUserBalance);

输出

[  {     name: “Jane”,     balance: 200.00  },  {     name: “John”,     balance: 110.50  }];

现在,我们从现实世界的例子开始,我们正在处理代表实体的对象。在这里,我们只是将每个用户拥有的金额翻了一番。我们这样做是为了提高可读性。

map 函数将对数组中包含的每个值一个一个地应用一个函数。结果将在一个新数组中返回。

您可能已经注意到在 doubleUserBalance 方法中使用了扩展语法。我们必须在每次迭代时创建一个新对象以保留初始对象,保持函数纯净并保持不变性。

4、更新数组的特定元素

要更新数组中的特定索引,我们将再次使用 map。

const users = [  {    name: "Jane",    balance: 100,  },  {    name: "John",    balance: 75,  },  {    name: "Ellis",    balance: 31.3,  },];
const double = (amount) => amount * 2;const indexToUpdate = 1; // we will change user at index 1 onlyconst nextUsers = users.map((user, index) => index === indexToUpdate ? { ...user, balance: double(user.balance) } : user);

输出

[   {      name: “Jane”,      balance: 100,   },   {      name: “John”,      balance: 150,   },   {      name: “Ellis”,      balance: 31.30   },];

在此示例中,我们使用传递给 map 的函数的第二个参数,即当前元素的索引。有了这些信息,我们就可以轻松地对感兴趣的索引进行操作。例如,在编写 redux reducer 时,这是一个非常巧妙的技巧。

为什么我们不简单地更新指定索引处的对象呢?

因为它违反了不变性原则。如果您在函数内部对临时数组进行操作,这没什么大不了的,尽管它可能会使其他开发代码的开发人员感到困惑,并使复杂函数的调试变得困难。

但是,如果您对不属于您的数据进行操作(数据所有权是从 C++ 智能指针借用的概念 ,您宁愿不改变对象。怀疑函数的纯度会导致代码难以维护,因此,请确保在创建不纯函数之前对它们有良好的约定。

5、删除数组的一部分

下一个经典的数组方法是filter。其目的是返回数组中满足谓词的所有元素。那些不这样做的人被拒之门外。

const users = [  {    name: "Jane",    balance: 100,  },  {    name: "John",    balance: 75,  },  {    name: "Ellis",    balance: 31.3,  },];
const hasEnoughMoney = (threshold) => (user) => user.balance >= threshold;const usersWithEnoughMoney = users.filter(hasEnoughMoney(50));

输出

[  {     name: “Jane”,     balance: 100,  },  {     name: “John”,     balance: 75,  },];

在这个示例中,我们还使用了一个返回函数的函数。这样,我们可以轻松地以声明方式自定义我们认为“足够”的阈值,例如,这可能会根据项目的规范而变化。这种分解传入多个函数的参数的方法称为柯里化。

加餐 删除重复项

filter方法和map方法一模一样,接受一个函数作为参数,这个函数有三个参数 当前值、当前值的索引和初始数组。

const elements = [1, 2, 3, 1, 4, 5, 2, 6, 6];const isUnique = (value, index, array) => array.indexOf(value) === index;const nextElements = elements.filter(isUnique); // [1, 2, 3, 4, 5, 6]

我们可以创建一个纯函数,仅当当前元素在数组中唯一时才返回 true。了解函数的工作原理是一个很好的练习,所以,我不会解释。

但是,您可能想检查 indexOf 的工作原理,特别是它返回的索引的属性。

警告 这种过滤独特元素的方法在性能方面很差。它可以用于小型阵列,但如果您受到限制,您可能需要选择更好的解决方案。此类问题通常是空间与时间的权衡,这是算法课程的主题。

6、计算元素的总和

现在,我们将介绍它们之父,全能的 reduce 函数,您可以使用它编写自己的 map 、 filter 以及所有其他 Array 函数。

reduce 方法包括将元素数组减少为单个值。它需要两个参数

一个函数,它的第一个参数 previousValue 是到目前为止累积的值,第二个参数 currentValue 是我们在这个数组中检查的当前值。它必须返回新的累加值。

一个初始值。在第一次迭代中,该值将用作previousValue。

const elements = [1, 2, 3, 4, 5];const nextElements = elements.reduce(  (previousValue, currentValue) => previousValue + currentValue,  0); // 15

这种方法只是将元素简化为其组成部分的总和。它本质上是添加它们,一次一个元素。

您是否注意到传递给 reduce 方法的函数有多简单?这是一个简单的总和!让我们将其重构为纯函数。

const elements = [1, 2, 3, 4, 5];const sum = (a, b) => a + b;
const nextElements = elements.reduce(sum, 0); // 15

这段代码的美妙之处在于它的可读性。您可以从字面上阅读它的作用 它将元素减少到它们的总和。

现在,我们已经对使用虚拟示例的 reduce 方法有了很好的理解,我们可以使用它来计算所有用户余额的总和。

const users = [  {    name: "Jane",    balance: 100,  },  {    name: "John",    balance: 75,  },  {    name: "Ellis",    balance: 31.3,  },];
const addBalance = (balance, user) => balance + user.balance;const balanceSum = users.reduce(addBalance, 0); // 206.3

它是编制统计数据和摘要的基础。这是您迟早必须要做的事情,使用 reduce 可以增强算法的风格和可读性。

7、 展平数组数组(可选

作为计算的结果,您可能会获得一个带有嵌套数组的数组。展平是将第 n 维数组转换为一维数组的操作。

如果这听起来不是很清楚,这里有一个使用原生 flat 方法的示例。

const elements = [1, [2], 3, [45], [6]];const nextElements = elements.flat(); // [1, 2, 3, 4, 5, 6]

为了简单起见,我们将一组数字展平。有趣的是,这也可以使用 reduce 手动实现。

function flatten(array) {  return array.reduce((previousValue, currentValue) => {    if (Array.isArray(currentValue)) {      // current value is an array, merge values and return the result      return [...previousValue, ...currentValue];    }
// current value is not an array, just add it to the end of the accumulation return [...previousValue, currentValue]; }, []);}
const elements = [1, [2], 3, [4, 5], [6]];const nextElements = flatten(elements); // [1, 2, 3, 4, 5, 6]

注意 不过,我没有看到很多用例。我可能曾经使用过这种技术来将原始 API 材料改编成更易读的东西,但我不相信你会经常需要它。

加餐 展平任意嵌套的数组

flat 方法采用一个 depth 参数,该参数控制数组展平的深度。默认情况下,它只展平第一个维度。如果您事先不知道它的维度,这里有一个完全展平数组的技巧。

const elements = [1, [2, [3, 4, [5], 6], [7, [8]]]];const result = elements.flat(Infinity); // [1, 2, 3, 4, 5, 6, 7, 8]

别担心,它不会无限循环 一旦数组被展平(当第一个维度的元素都不是数组时 ,它就会停止。

8、在特定索引处添加元素

我们之前看到了一种在任意位置删除元素的方法。我们可以利用这些知识并重用切片方法在特定索引之后插入元素。

const elements = [1, 2, 4, 5];const indexToInsert = 2; // we will append the element at index 2const elementToInsert = 3; // we will insert the number "3"
const nextElements = [ ...elements.slice(0, indexToInsert), elementToInsert, ...elements.slice(indexToInsert),]; // [1, 2, 3, 4, 5]

这种方法尊重我们对纯度和不变性的要求,尽管它总共创建了三个数组。

还有另一种方法可以做到这一点,在某些方面,这违反了这些原则。

const elements = [1, 2, 4, 5];const indexToInsert = 2; // we will append the element at index 2const elementToInsert = 3; // we will insert the number "3"
const nextElements = elements.reduce((previousValue, currentValue, index) => { previousValue.push(currentValue); // ?? if (index === indexToInsert) { previousValue.push(elementToInsert); // ???? } return previousValue;}, []);

我们使用reduce来循环遍历元素,并使用push构建我们的新数组,push是一种改变数组的方法,因此违反了不变性原则。

这样做很好,因为我们实际上是在改变一个专门为此目的创建的数组。这个数组由传递给 reduce 函数的函数拥有,即使它是由调用者初始化的。

所有这些都是传统的 您永远不需要读取作为 reduce 函数的第二个参数传递的任何内容,因为它只是作为 reduce 函数在其中累积值的地方。

所以,是的,它违反了不变性原则,但仅限于局部。由于计算机的工作方式(具体而言,RAM 在设计上是可变的 ,在现实世界场景中的纯不变性是不可能的。

这种对原则的违反使我们能够在空间和时间上保持最佳性能

Time 我们在数组中线性循环,只有一次。

Space 我们只使用了一个数组,这是最终的结果。

不变性是一个真正重要的原则,但您可能必须在本地违反它(因此,将其发生率降低到几乎为零 以提高此类低级别区域的性能。

注意 将这种“脏”算法包装在函数中是一个好主意,充当黑盒,调用者只需知道传递的对象不会被改变,除非另有说明。

9、 对数组进行排序

知道如何在 JavaScript 中对数组进行排序是数组操作的基础。幸运的是,我们可以随时使用一种实现。

默认值sort有一个小问题 它改变了数组。嗯,这不完全是一个问题,而是一个优化空间的设计决策,而且是可以理解的。希望在使用 sort 时有一个巧妙的技巧可以让我们非常轻松地保持初始数组的健全 slice 方法。

const elements = [1, 6, 4, 5, 2, 3];const byOrderAsc = (a, b) => a b;const nextElements = elements.slice().sort(byOrderAsc); // [1, 2, 3, 4, 5, 6]

slice 方法在未指定参数时创建整个数组的副本,充当副本创建者。然后我们使用副本的排序方法将元素从小到大排序。然后 sort 方法返回复制数组,保持我们的初始数组不变。

一个奇怪的命名方案

你可能已经注意到名字的函数是 byOrderAsc 。命名函数的方法有很多种,我们现在知道命名是计算机科学中最难的两门学科之一。

当我们看这个函数时,它是两个值之间的简单差异。这就是我最初给它起的名字 difference。但是这个名称传达了功能的机制而不是其含义。而且我相信这样代码会更清晰。

在设计函数时考虑这种权衡 是理解原因(为什么 很重要的高级代码,还是处理算法及其细节(如何 的低级代码?

10、生成任意大小的数组

我经常需要生成大小从 0 到 n 的空数组。主要用于构建选项卡和列表。

由于它经常发生,我设计了一种方法来在一行中构建数组。

const elements = Array(10).fill().map((a, i) => i);// [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

这条简单的车道做了三件事

  • 它创建了一个包含 10 个空槽的数组。实际上,它将length属性设置为 10 ,但数组本身包含 0 个元素。直接通过它进行映射是行不通的,因为映射会遍历数组本身并丢弃length属性。

  • 自己试试 用 Array(10) 初始化一个数组,它会打印 <10 empty slots> 。

  • 然后它用 undefined 填充数组,填充数组以包含我们需要的 10 个元素。

然后可以遍历元素。在这里,我们使用 map ,函数的第二个参数是当前迭代中的当前索引。这有效地使用从 0 到 9 的数字填充数组。

然后,您可以创建一个包装函数以在 Array 构造函数中传递您想要的任何值,从而创建任意数量的插槽。

11、寻找元素

使用数组时一个非常常见的用例是找到一个满足谓词的元素, 者换句话说,它符合我们的期望。

与 filter 类似,find 方法在数组中查找元素并返回它。事实上,它本质上就像 filter 一样工作,只是它将结果减少为单个值而不是数组。

const users = [  {    id: 1,    username: "supercoder",  },  {    id: 2,    username: "xXxSniperElitexXx",  },  {    id: 3,    username: "sPoNgEbOb_cAsE",  }];
const withId = (id) => (user) => user.id === id;const userWithId = users.find(withId(1));

输出

{   id: 1,   username: “supercoder }

在这个例子中,我们设置了一个实用方法 withId 只是为了方便。再一次,我们获得了可读性,并且 users.find(withId(1)) 可以从左到右以完美的英语句子阅读。

加餐 返回索引而不是对象本身

您有时可能需要索引(对象在数组中的位置 而不是对象本身。例如,您可能希望将索引保留在某处以供以后检索, 者仅返回该对象索引之后的数组部分。

这可以通过 findIndex 方法实现。这是相同的代码,使用 findIndex 而不是 find

const users = [  {    id: 1,    username: "supercoder",  },  {    id: 2,    username: "xXxSniperElitexXx",  },  {    id: 3,    username: "sPoNgEbOb_cAsE",  }];
const withId = (id) => (user) => user.id === id;const userWithId = users.findIndex(withId(1)); // 0

如您所见,它产生对象的索引,即 0。

在使用 React Redux 移动应用程序时,我开始使用这种方法的频率至少与使用常规 find 方法的频率一样。

12、检查所有值是否满足predicate

让我们回到现实生活中的例子。想象一下,你有一群冠军,你想确保他们在与 Boss 战斗之前都达到最低等级。有一个很好的方法,它被称为every。

const champions = [  {    name: "Darius",    level: 7,  },  {    name: "Katarina",    level: 12,  },  {    name: "Swain",    level: 9,  }];
const hasExpectedLevel = (expectedLevel) => (champion) => champion.level >= expectedLevel;const hasUltimate = hasExpectedLevel(6);const chamionsAllHaveUltimate = champions.every(hasUltimate) / true

在这里,该方法根据一个称为predicate的函数检查数组的每个条目(与filter方法完全相同 。为了清楚起见,我已将此片段分解为不同的函数。

这个就没什么好说的,每一种方法都简单易懂。

13、检查至少一个值是否满足predicate

想象一下,规格发生了一点变化 不再需要所有冠军都拥有终极技能。一个就够了,但至少要有一个。这很简单,您只需将每个替换为一些。

const champions = [  {    name: "Darius",    level: 7,  },  {    name: "Katarina",    level: 4,  },  {    name: "Swain",    level: 5,  }];
const hasExpectedLevel = (expectedLevel) => (champion) => champion.level >= expectedLevel;const hasUltimate = hasExpectedLevel(6);const oneChampionHasUltimate = champions.some(hasUltimate) / true

加餐 这都是数学的

这些函数,every 和 some,是同一枚硬币的两个面。它们有它们的数学等价性,分别是全称量化和存在量化。它们是称为predicate逻辑数学分支的一部分,通常在离散数学中进行研究,作为证明理论课程的一部分。

证明理论是数学的一个主要分支,因为它有助于为未来的发现奠定基础,同时确保我们的知识是健全的。

如果您好奇,您可以阅读有关离散数学的十几本书中的一本。当然,对于你的日常 JavaScript 编程来说,这不是必需的,所以,我不再过分强调这一点。

总结

我们已经对这些示例进行了大量工作,但我们并未涵盖所有 Array 的可能性。例如

FlatMap 它首先通过一个元素数组进行映射(期望映射函数将返回一个数组 ,然后将映射的结果展平。这个名字比 Smoosh 更好地传达了机制。

At 它返回指定索引处的元素。它主要用于处理负值(从末尾开始 。

Concat 合并两个 多个数组。不再有用,您应该使用扩展语法。

你可以很容易地理解它们的作用,它们是如何工作的,但是它们在你的日常 JavaScript 编程中并不是那么有用。

最后,我们已经在本文中看到了大多数重要的 Array 特性。但对于狂热的读者,这里有一些后续步骤

  • Promises 当使用一组 Promise(例如,请求 时,您不能使用常规数组方法对结果进行异步处理。Promise 对象提供了一些方便的特性,例如 Promise.all 和 Promise.any(记住谓词逻辑 。

  • Observables 这是集合异步编程的下一步(数组的另一个更高级的名称 。它是名为反应式编程的编程范式的一部分,并将随时间推移的事件流视为集合,它提供了我们刚刚看到的许多 Array 工具。把头绕在它周围是相当困难的,但非常值得。

  • 不可变集合 很像默认的 Array 对象,库提供了它们的等价物,但不可变。也就是说,每个操作都会产生一个新数组。这些库中的大多数都力求使其尽可能高效,细节超出了本文的范围。最流行的例子是 ImmutableJS。对于 React 开发来说,理解和掌握它是一个非常重要的概念,与 memoization 一起,是提高应用程序性能的关键。

最后的话

我希望你喜欢这篇文章。如果您觉得有帮助,请点赞分享,如果您还有什么问题,请在留言区给我留言,我会尽快回复的!



学习更多技能

请点击下方

小羊羔锚文本外链网站长https://seo-links.cn 
回复列表
默认   热门   正序   倒序

回复:13 个JavaScript 数组小技巧,让你更像技术专家

Powered by 小羊羔外链网 8.3.7

©2015 - 2024 小羊羔外链网

免费发软文外链 鄂ICP备16014738号-6

您的IP:100.24.20.141,2024-03-28 20:11:19,Processed in 0.05283 second(s).

支持原创软件,抵制盗版,共创美好明天!
头像

用户名:

粉丝数:

签名:

资料 关注 好友 消息