循环遍历数组并删除项目,而不中断for循环

637

我有以下的for循环,当我使用splice()删除一个项后,接下来就会出现“seconds未定义”的情况。 我可以检查它是否未定义,但我觉得可能有更优雅的方式来解决这个问题。我的目标是简单地删除一个项并继续执行。

for (i = 0, len = Auction.auctions.length; i < len; i++) {
    auction = Auction.auctions[i];
    Auction.auctions[i]['seconds'] --;
    if (auction.seconds < 0) { 
        Auction.auctions.splice(i, 1);
    }           
}

14
除了反向迭代和调整长度之外,您还可以将所需成员放入新数组中。 - RobG
3
为什么你要使用Auction.auctions[i]['seconds']--而不是auction.seconds--呢? - Don Hatch
你可能想要查看预定义函数 .shift()。 - Rakushoe
18个回答

1072

当你使用.splice()时,数组正在重新索引,这意味着当一个索引被删除时,你将跳过一个索引,并且你缓存的.length已经过时。

要解决这个问题,你需要在.splice()之后递减i,或者简单地反向迭代...

var i = Auction.auctions.length
while (i--) {
    ...
    if (...) { 
        Auction.auctions.splice(i, 1);
    } 
}

这样重新索引就不会影响迭代中的下一项,因为索引仅影响从当前点到数组末尾的项目,而迭代中的下一项低于当前点。


1
在我的疑惑中,如果length === 0将会导致一个无限循环,我尝试了这个解决方案(它当然是有效的),因为它首先计算i的值,然后递减。然而,--(和++)在行为上非常奇怪,以至于像Swift这样的现代语言不再支持它们。 我认为它们是不好的做法(至少在这种情况下)。 - lukas_o
9
如果你理解了它的含义,那么i++并没有任何奇怪或意外的功能。它的意思是先对值进行评估,然后再将其递增。而++i则意味着先递增该值,然后再对其进行评估。JavaScript不会做任何超出这个范围之外的事情。这非常容易理解,并且保证每次都以完全相同的方式工作,即使你使用不同的JS引擎也是如此。 - aggregate1166877

252

这是一个相当常见的问题。解决方法是向后循环:

for (var i = Auction.auctions.length - 1; i >= 0; i--) {
    Auction.auctions[i].seconds--;
    if (Auction.auctions[i].seconds < 0) { 
        Auction.auctions.splice(i, 1);
    }
}

如果你从结尾弹出它们也没关系,因为在向后移动时索引会被保留。


60

在循环过程中重新计算长度,而不仅仅是在开头进行计算,例如:

for (i = 0; i < Auction.auctions.length; i++) {
      auction = Auction.auctions[i];
      Auction.auctions[i]['seconds'] --;
      if (auction.seconds < 0) { 
          Auction.auctions.splice(i, 1);
          i--; //decrement
      }
}

这样你就不会超出范围了。

编辑:在if语句中添加了一个减量操作。


57
尽管您的问题是关于从正在迭代的数组中删除元素而不是高效地删除元素(以及其他一些处理),但如果处于类似情况,我认为应该重新考虑它。
这种方法的算法复杂度为O(n^2),因为splice函数和for循环都遍历数组(splice函数在最坏情况下移动数组的所有元素)。相反,您可以将所需元素推入新数组,然后将该数组分配给所需变量(刚刚迭代的变量)。
var newArray = [];
for (var i = 0, len = Auction.auctions.length; i < len; i++) {
    auction = Auction.auctions[i];
    auction.seconds--;
    if (!auction.seconds < 0) { 
        newArray.push(auction);
    }
}
Auction.auctions = newArray;

自从ES2015以来,我们可以使用 Array.prototype.filter 将其全部放在一行中:
Auction.auctions = Auction.auctions.filter(auction => --auction.seconds >= 0);

28
Auction.auctions = Auction.auctions.filter(function(el) {
  return --el["seconds"] > 0;
});

24

如果您正在使用ES6+,为什么不直接使用Array.filter方法?

Auction.auctions = Auction.auctions.filter((auction) => {
  auction['seconds'] --;
  return (auction.seconds > 0)
})  

请注意,在过滤迭代期间修改数组元素只适用于对象,而不适用于原始值的数组。


17

下面是这个简单的线性时间问题的简单线性时间解决方案。

当我运行这段代码,并且n = 100万时,每次调用filterInPlace()需要0.013到0.016秒。一个二次方的解决方案(如被接受的答案)将需要一个百万倍的时间,或者更多。

// Remove from array every item such that !condition(item).
function filterInPlace(array, condition) {
   var iOut = 0;
   for (var i = 0; i < array.length; i++)
     if (condition(array[i]))
       array[iOut++] = array[i];
   array.length = iOut;
}

// Try it out.  A quadratic solution would take a very long time.
var n = 1*1000*1000;
console.log("constructing array...");
var Auction = {auctions: []};
for (var i = 0; i < n; ++i) {
  Auction.auctions.push({seconds:1});
  Auction.auctions.push({seconds:2});
  Auction.auctions.push({seconds:0});
}
console.log("array length should be "+(3*n)+": ", Auction.auctions.length)
filterInPlace(Auction.auctions, function(auction) {return --auction.seconds >= 0; })
console.log("array length should be "+(2*n)+": ", Auction.auctions.length)
filterInPlace(Auction.auctions, function(auction) {return --auction.seconds >= 0; })
console.log("array length should be "+n+": ", Auction.auctions.length)
filterInPlace(Auction.auctions, function(auction) {return --auction.seconds >= 0; })
console.log("array length should be 0: ", Auction.auctions.length)

请注意,这将修改原始数组而不是创建新数组;像这样直接进行操作可能是有利的,例如在数组是程序的唯一内存瓶颈的情况下;在这种情况下,您不想创建另一个相同大小的数组,即使是暂时的。

15

另一种简单的方法来遍历一个数组元素:

while(Auction.auctions.length){
    // From first to last...
    var auction = Auction.auctions.shift();
    // From last to first...
    var auction = Auction.auctions.pop();

    // Do stuff with auction
}

15
普通的for循环对我来说更熟悉,每次从数组中删除一个项时,我只需要递减索引。

//5 trues , 5 falses
var arr1 = [false, false, true, true, false, true, false, true, true, false];

//remove falses from array
for (var i = 0; i < arr1.length; i++){
    if (arr1[i] === false){
        arr1.splice(i, 1);
        i--;// decrement index if item is removed
    }
}
console.log(arr1);// should be 5 trues


2
我认为用splice和decrement方法来解决for-loop中小的边缘情况,考虑到熟悉程度,是最具可移植性/易理解性的方法,而不需将循环重写成filter/reduce/flatmap等或者倒叙循环(这可能会在6个月后被忽略)。其他解决方案可能更好/更聪明,但有时只需要修补一下小的循环边缘情况。 - PotatoFarmer

11

这里是splice正确使用的另一个示例。这个示例是关于从“array”中删除“attribute”的。

for (var i = array.length; i--;) {
    if (array[i] === 'attribute') {
        array.splice(i, 1);
    }
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接