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

1baf87d0b62d954ead8dc7b928530d79.png

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

翻译 | 杨小爱

我们每天都在处理集合,通常是处理从 API 提供的元素列表。但是,这些 API 可能不会返回我们想要的结果或我们感兴趣的形状。

因此,在客户端 JavaScript 中操作数组是很常见的。在解决这些问题的所有不同方法中,我们将使用最好的方法:函数式编程函数。

我们不会在本文中介绍函数式编程的基础知识,因此,您可能需要阅读本文以外的一些知识,以获取有关此主题的入门知识。

我们的工作将尊重以下假设:

简洁性:我们不会改变任何对象,并且总是会返回新对象。

不变性:我们的方法不会改变数组的状态,而是返回一个与我们的规范相对应的新状态。

以下是我在撰写本文时遵循的一些约定:

有些观点会迅速谈论可以完成相同工作的另一种方式,以及未使用它的原因。

当我觉得它可以带来一些有价值的东西时,我会提供一些奖励积分。

有些点是可选的,但我相信值得一提。

准备好了吗?我们现在开始吧!

1、在边缘添加一个元素

让我们从简单的开始。它利用扩展语法来精美地传达其在末尾附加元素的意图。

const elements = [1, 2, 3, 4];
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 element
const lastElementRemoved = elements.slice(0, 4); // [1, 2, 3, 4]


// remove first element
const 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 element
const 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 users
const 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 only
const 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, [4, 5], [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 2
const 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 2
const 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 一起,是提高应用程序性能的关键。

最后的话

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

学习更多技能

请点击下方公众号

95e0da1126ce34dc5b00b512dbb1ebc6.gif

1651f79b5567a51500c6d662321c4119.png

f057e9c353253a8bddd5c717e6cb44c3.png

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值