如何在 splice() 重新排序时从数组中删除对象

How to remove objects from array when splice() reorders

我有一个 Javascript 对象数组。我正在使用 forEach 遍历数组并检查特定匹配项,如果该匹配项为真,则 splicing/removing 从当前索引处的数组中获取它。

我疯了吗?
当我从数组中找到一个匹配项并且 splice/remove 该对象时,数组似乎在自动移动,导致随着索引的增加而跳过下一个项目。这是真的?如果是这样,循环和测试数组中每个 ALL 对象并删除 ALL 匹配项的最准确方法是什么?

代码示例(请注意,这显然不是一个对象数组,只是一个简单的示例):

var fruits = ["Banana", "Orange", "Apple", "Mango"]
document.getElementById("demo").innerHTML = fruits;

function myFunction() {
    fruits.forEach(function(fruit, index) { 
      if(fruit == "Banana" || fruit == "Orange" )       {
        fruits.splice(index, 1);
      }
    })
    document.getElementById("demo").innerHTML = fruits;
}
myFunction();

结果: 橘子、苹果、芒果

在该示例中,香蕉被删除,因此 "shifting" 橙色变为 0。此时,索引然后递增到 1,跳过 0,或完全跳过橙色,并继续循环。

此时我唯一的解决办法是用某物替换已删除的项目,以防止"shift"。

拼接示例

fruits.splice(index, 1, "deleted");

结果

已删除,已删除,苹果,芒果

我的问题

这是通过循环从数组中删除 items/objects 的 best/most 准确方法吗?

感谢您的宝贵时间和建议。

当您修改 forEach 中使用的基础数组时,您不能指望此代码能够正常工作,MDN Array.prototype.forEach 作为本案例的一部分。

相反,您可以使用 Array.prototype.filter 或传统的 for 语句。

一般来说,由于您上面提到的原因,改变您当前正在迭代的数组不是一个好主意。您的代码打算做的是函数式编程中称为过滤器的非常常见的用例。阅读 Array.prototype.filter 上的文档。将数组和谓词函数传递给过滤器函数以确定该元素是否要包含在过滤结果中。过滤器函数将复制原始数组,不包含不需要的元素。

示例代码:

function myFunction() {
  var filteredFruits = fruits.filter(function(fruit) { 
    return fruit !== "Banana" && fruit !== "Orange";
  });
  // filteredFruits = ["Apple", "Mango"]
  document.getElementById("demo").innerHTML = filteredFruits;
}
myFunction();

过滤是解决方案。 试试这个

var fruits = ["Banana", "Orange", "Apple", "Mango"];
    function myFunction() {
        var newFruitAray = fruits.filter(function(fruit) {
            return fruit !== "Banana" && fruit !== "Orange";
        });

       document.getElementById("demo").innerHTML = newFruitAray;
    }
    myFunction();

不,你没有生气。

正如您所描述的,从集合中删除一项确实会导致在索引递增时跳过下一项。此外,docs 表示不会为已删除或未初始化的索引属性(即 undefined)调用 forEach 回调。

问题

为了避免在索引递增时跳过,您可以考虑使用一个计数器变量,您可以在每次删除项目时递增该变量。然后每次访问一个项目时,使用计数器向后移动索引,如下所示:

var fruits = ["Banana", "Orange", "Apple", "Mango"]

function myFunction() {

    var itemsRemoved = 0; // remember how many items you deleted

    fruits.forEach(function(fruit, index, a) { 

      var shiftedIndex = index - itemsRemoved; // update the index

      fruit = fruits[shiftedIndex];

      if(fruit == "Banana" || fruit == "Orange") {
        fruits.splice(shiftedIndex, 1); // item removed
        itemsRemoved++; // update counter
      }

    })

}

myFunction();

// fruits: ['Apple', 'Mango']

此 hack 将删除 BananaOrange,但是,它有一个主要缺点:forEach 回调仅对数组中的每个项目执行一次。每次删除一个项目时,数组长度递减 1。因此,如果您有 4 个项目并且在迭代时删除 2 个,那么 forEach 循环只会 运行 两次,跳过最后两个数组项。这解释了为什么如果您尝试删除 BananaOrangeApple:

将不会删除 Apple

循环 1:

  • 已执行:
  • 水果:香蕉、橙子、苹果、芒果
  • 长度:4
  • 删除: 索引 0
  • 移除后的长度:3

循环 2:

  • 已执行:
  • 水果:橙子、苹果、芒果
  • 长度:3
  • 删除: 索引 0
  • 移除后的长度:2

循环 3:

  • 已执行:
  • 水果:苹果、芒果
  • 长度:2
  • 删除: n/a
  • 删除后的长度:n/a

循环 3 永远不会执行,因为到那时循环已经 运行 两次。它不会 运行 第三次,因为在删除 BananaOrange 之后,数组长度现在是 2 并且 forEach 每个数组项只会 运行 一次。

要点

一般来说,当您从集合中删除一个项目的同时对其进行迭代时,迭代的结果是不确定的,并且可能会产生奇怪的行为、不明确的错误和静默失败。在这些情况下,某些语言甚至会抛出并发异常。

外卖

此处正确的方法是将您想要的项目从现有数组复制到新数组中,忽略您不需要的项目。然后你可以丢弃旧数组或用新数组覆盖它:

var fruits = ["Banana", "Orange", "Apple", "Mango"]

function myFunction() {

    var newFruits = [];

    fruits.forEach(function(fruit, index){

      if(fruit != "Banana" && fruit != "Orange"){

        newFruits.push(fruit);

      }

    })

    fruits = newFruits;

}

myFunction();

// fruits: ['Apple', 'Mango']

要删除对象,同样的想法也适用,只是您将针对对象属性而不是字符串进行检查:

var fruitObjects = [{name:"Banana"},{name:"Orange"},{name:"Apple"},{name:"Mango"}];

function removeFruitObjects(){

  var newFruits = [];

  fruitObjects.forEach(function(fruit, index){

      if(fruit.name != "Banana" && fruit.name != "Orange"){

        newFruits.push(fruit);

      }

    })

  fruitObjects = newFruits;

}

removeFruitObjects(); 

// fruitObjects: [ { name: 'Apple' }, { name: 'Mango' } ]