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。

onst 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 方法返回复制数组,保持我们的初始数组不变。

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)) 可以从左到右以完美的英语句子阅读。

 

 

 

 

© 版权声明
THE END
喜欢就支持一下吧
点赞8 分享
抢沙发
头像
欢迎您留下宝贵的见解!
提交
头像

昵称

取消
昵称表情代码图片