在使用foreach循环时改变数组中的值

466

例子:

var arr = ["one","two","three"];

arr.forEach(function(part){
  part = "four";
  return "four";
})

alert(arr);

这个数组的值仍然是原来的,有没有办法从迭代函数中对数组元素进行写入访问?


1
相关:https://dev59.com/b1fUa4cB1Zd3GeqPH26g - Pacerier
5
尝试使用map方法(https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Array/map):`x=[2,3,4]; x=x.map(n=>n*2); // [4,6,8]` - Justin
11个回答

721
回调函数将传入元素、索引和数组本身。
arr.forEach(function(part, index, theArray) {
  theArray[index] = "hello world";
});

编辑 - 如评论中所述,.forEach()函数可以接受第二个参数,它将在每次回调调用时用作this的值:

arr.forEach(function(part, index) {
  this[index] = "hello world";
}, arr); // use arr as this

第二个例子展示了在回调函数中将arr本身设置为this。有人可能会认为在.forEach()调用中涉及的数组可能是默认this值,但出于某种原因它不是这样的;如果没有提供第二个参数,this将是undefined

(注意:如果回调函数是一个=>函数,则上述关于this的内容不适用,因为当调用此类函数时,this永远不会绑定到任何内容。)

另外重要的是要记住,在数组原型上提供了一整套类似的实用工具,许多关于其中一个函数或另一个函数的问题在Stackoverflow上出现,以至于最好的解决方案是选择一个不同的工具。你有:

  • forEach 用于对数组中的每个条目执行某些操作;
  • filter 用于生成仅包含符合条件的条目的新数组;
  • map 通过转换现有数组来生成一对一新数组;
  • some 用于检查数组中是否至少有一个元素符合某个描述;
  • every 用于检查数组中是否所有条目都匹配某个描述;
  • find 在数组中查找值

等等。 MDN链接


69
ES6: arr.forEach((o, i, a) => a[i] = myNewVal)谢谢! - DiegoRBaquero
6
为了完整起见,.forEach() 还接受第二个参数 thisArg,你可以在回调函数内使用它作为 this。注意:这是 .forEach 的一个参数,而不是回调函数的参数。 - Moha the almighty camel
4
为了使用作为.forEach()方法第二个参数传递的this对象,你需要使用语法function()传递回调函数,因为使用ES6的箭头函数() => {}不能绑定上下文。 - jpenna
1
包含第三个参数theArray的目的是什么?你不能这样做吗:arr[index] = "hello world"; - Dennis
1
@Dennis 是的,如果涉及到的数组位于本地作用域中。有时显式传递引用是有用的情况。不可否认,我很少发现需要这样做。尽管如此,API就是这样工作的。 - Pointy
显示剩余6条评论

161

让我们试着简单明了地讨论它是如何工作的。这与变量类型和函数参数有关。

这是我们要讨论的代码:

var arr = ["one","two","three"];

arr.forEach(function(part) {
  part = "four";
  return "four";
})

alert(arr);

首先,这里是你应该阅读 Array.prototype.forEach() 的内容:

https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/forEach

其次,让我们简要介绍 JavaScript 中的值类型。

原始类型(undefined、null、String、Boolean、Number)存储实际值。

例如:var x = 5;

引用类型(自定义对象)存储对象的内存位置。

例如:var xObj = { x : 5 };

第三,函数参数的工作方式。

在函数中,参数始终按值传递。

因为 arr 是一个字符串数组,它是由原始对象组成的数组,这意味着它们是按值存储的。

因此,在上面的代码中,这意味着每次 forEach() 迭代时,part 等于与 arr[index] 相同的值,但不是相同的对象。

part = "four"; 将更改 part 变量,但不会影响到 arr

以下代码将更改所需的值:

var arr = ["one","two","three"];

arr.forEach(function(part, index) {
  arr[index] = "four";
});

alert(arr);

如果数组 arr引用类型的数组,下面的代码将会运行成功,因为引用类型存储的是对象的内存地址而不是实际对象。

var arr = [{ num : "one" }, { num : "two"}, { num : "three"}];

arr.forEach(function(part, index) {
  // part and arr[index] point to the same object
  // so changing the object that part points to changes the object that arr[index] points to

  part.num = "four";
});

alert(arr[0].num);
alert(arr[1].num);
alert(arr[2].num);
下面的示例说明您可以将part指向一个新对象,同时保留存储在arr中的对象:
var arr = [{ num : "one" }, { num : "two"}, { num : "three"}];

arr.forEach(function(part, index) {
  // the following will not change the object that arr[index] points to because part now points at a new object
  part = 5;
});

alert(arr[0].num);
alert(arr[1].num);
alert(arr[2].num);

2
确实,解释得非常好!如果能在其他迭代方法(如for...of、map等)上进行扩展,那就更好了。新的for...of对于这种情况的工作方式与forEach非常相似,只是缺少“索引”以便最终修改原始数组(就像在forEach中一样)。此外,ES5的map似乎与forEach类似,可能只是从语法角度来看,使用map返回值的可能性使事情更加清晰,而不是使用索引。 - Christophe Vidal
1
很好的解释。谢谢你。毕竟我还是不明白这个语句的意思: 在函数中,参数总是按值传递。第二个例子呢? - Alex
@7sides,这个语句描述了函数参数总是通过值传递。对于基本类型,它将是基本类型指向的值。而对于对象,它将是对象指向的位置。 这个w3schools页面提供了一个很好的解释。请看参数通过值传递对象通过引用传递两个部分。 - Dave
在函数中,参数总是按值传递的。完成。谢谢。+1 - radarbob

155

数组:[1, 2, 3, 4]
结果:["foo1", "foo2", "foo3", "foo4"]

Array.prototype.map() 保留原始数组

const originalArr = ["Iron", "Super", "Ant", "Aqua"];
const modifiedArr = originalArr.map(name => `${name}man`);

console.log( "Original: %s", originalArr );
console.log( "Modified: %s", modifiedArr );

Array.prototype.map() 覆盖原始数组

let originalArr = ["Iron", "Super", "Ant", "Aqua"];
originalArr = originalArr.map(name => `${name}man`);

console.log( "Original: %s", originalArr );

Array.prototype.forEach() 覆盖原始数组

const originalArr = ["Iron", "Super", "Ant", "Aqua"];
originalArr.forEach((name, index) => originalArr[index] = `${name}man`);

console.log( "Overridden: %s", originalArr );


5
.map() 不会修改原始数组,它会创建一个新的数组。重新分配变量并不会改变原始对象。 - vadkou
1
let arr1 = ["1", 2, 3, 4];arr1.map(function(v) { return "foo"+ v; });console.log( arr );Array.prototype.map() 不会修改原始数组,而forEach则会改变。 - Anupam Maurya
@AnupamMaurya 这完全不正确。map 绝对可以改变其数组,而 forEach 通常不会这样做。 - Sebastian Simon
2
@SebastianSimon,感谢您的回复。我想补充的是,forEach会覆盖数据,但是map不会,它会创建一个新的副本。 - Anupam Maurya
1
比其他人更清晰 - Yang Wang

17

Javascript 是传值的,这意味着 part 实际上只是数组中值的一个副本

如果要改变这个值,在循环中需要直接访问数组本身。

arr[index] = 'new value';


1
这取决于part的类型是否被复制 - 尽管您是正确的,该变量不是指针,而是一个值。 - Bergi
8
说 "JavaScript是按值传递" 是一个粗略的概括。在JavaScript中有引用类型。 值类型是按值传递的。 - Alex Turpin
12
不是粗略的概括,而是完全正确和绝对的陈述。JavaScript通过传值引用。JavaScript始终是按值传递的。 - hvgotcodes
1
@Bergi:不,类型并不重要。在赋值时所有的值都会被复制——在JavaScript中唯一存在的值是原始值和引用值。原始值和引用值在赋值时都会被复制。 - newacct
7
即使一种语言有所谓的引用(reference),也不意味着它使用按引用传递(pass by reference)。引用可以(并且经常)被按值传递,即引用的值被复制,然后该副本被用作参数。 - hvgotcodes
显示剩余2条评论

12

这是一个类似的答案,使用一个=>样式的函数:

var data = [1,2,3,4];
data.forEach( (item, i, self) => self[i] = item + 10 );

给出结果:

[11,12,13,14]

箭头函数中self参数并不是绝对必要的,因此

data.forEach( (item,i) => data[i] = item + 10);

也有效。


5

forEach函数可以有一个回调函数(eachelement, elementIndex)。因此,你需要做的是:

arr.forEach(function(element,index){
    arr[index] = "four";   //set the value  
});
console.log(arr); //the array has been overwritten.

如果你想保留原始数组,那么在进行上述过程之前可以先复制它。

要复制,可以使用以下代码:

var copy = arr.slice();

3
如果你想复制一个数组,请使用 map() 而不是 forEach()map() 迭代源数组并返回一个包含[修改后]原始数组副本的新数组:源数组保持不变。 - Nicholas Carey

3

如果要完全添加或删除元素,这将改变索引,可以通过扩展zhujy_8833的建议使用slice()在副本上迭代,只需计算您已删除或添加的元素数量,并相应地更改索引。例如,要删除元素:

let values = ["A0", "A1", "A2", "A3", "A4", "A5", "A6", "A7", "A8"];
let count = 0;
values.slice().forEach((value, index) => {
    if (value === "A2" || value === "A5") {
        values.splice(index - count++, 1);
    };
});
console.log(values);

// Expected: [ 'A0', 'A1', 'A3', 'A4', 'A6', 'A7', 'A8' ]

在之前插入元素:

if (value === "A0" || value === "A6" || value === "A8") {
    values.splice(index - count--, 0, 'newVal');
};

// Expected: ['newVal', A0, 'A1', 'A2', 'A3', 'A4', 'A5', 'newVal', 'A6', 'A7', 'newVal', 'A8' ]

在元素后插入:

if (value === "A0" || value === "A6" || value === "A8") {
    values.splice(index - --count, 0, 'newVal');
};

// Expected: ['A0', 'newVal', 'A1', 'A2', 'A3', 'A4', 'A5', 'A6', 'newVal', 'A7', 'A8', 'newVal']

替换元素:
if (value === "A3" || value === "A4" || value === "A7") {
    values.splice(index, 1, 'newVal');
};

// Expected: [ 'A0', 'A1', 'A2', 'newVal', 'newVal', 'A5', 'A6', 'newVal', 'A8' ]

注意:如果要实现“before”和“after”插入项,代码应该先处理“before”插入项,否则结果可能不如预期。

3

将其替换为数组的索引。

array[index] = new_value;

2
使用Array对象方法可以修改数组内容,但与基本的for循环相比,这些方法缺少一个重要的功能。您无法在运行时修改索引。
例如,如果您要删除当前元素并将其放置到同一数组中的另一个索引位置,则可以轻松完成此操作。如果您将当前元素移动到先前位置,则下一次迭代没有问题,您将获得与未进行任何操作相同的下一个项目。
请考虑此代码,在索引计数达到5时,我们将索引位置5的项移动到索引位置2。
var ar = [0,1,2,3,4,5,6,7,8,9];
ar.forEach((e,i,a) => {
i == 5 && a.splice(2,0,a.splice(i,1)[0])
console.log(i,e);
}); // 0 0 - 1 1 - 2 2 - 3 3 - 4 4 - 5 5 - 6 6 - 7 7 - 8 8 - 9 9

然而,如果我们将当前元素移动到当前索引位置之后的某个位置,事情就会变得有些混乱。接下来的项目将进入移动项目的位置,在下一次迭代中,我们将无法看到或评估它。

考虑以下代码,在索引计数到5时,将位于索引位置5的项移动到索引位置7。

var a = [0,1,2,3,4,5,6,7,8,9];
a.forEach((e,i,a) => {
i == 5 && a.splice(7,0,a.splice(i,1)[0])
console.log(i,e);
}); // 0 0 - 1 1 - 2 2 - 3 3 - 4 4 - 5 5 - 6 7 - 7 5 - 8 8 - 9 9

所以我们从未在循环中遇到过6。通常在for循环中,当你向前移动数组项时,你需要递减索引值,这样你的索引就会在下一次运行中保持相同的位置,你仍然可以评估移动到删除项位置的项。但是,使用数组方法无法实现这一点。你不能改变索引。请检查以下代码:

var a = [0,1,2,3,4,5,6,7,8,9];
a.forEach((e,i,a) => {
i == 5 && (a.splice(7,0,a.splice(i,1)[0]), i--);
console.log(i,e);
}); // 0 0 - 1 1 - 2 2 - 3 3 - 4 4 - 4 5 - 6 7 - 7 5 - 8 8 - 9 9

当我们减少i时,它不会从5继续,而是从离开的6开始。

所以请记住这一点。


2

如果您想删除项目

如果您需要完全从列表中删除一个或多个项目,则最好使用for循环并向后遍历该循环。

请注意,要向后遍历循环。
    for (let i = myArray.length - 1; i >= 0; i--)
    {
        const item = myArray[i];

        if (...) 
        {
           // remove item
           // https://dev59.com/CG025IYBdhLWcg3w1ZoL?rq=1
        }
    };

倒序遍历意味着每个项目的数组都不会改变。如果您通过循环向前遍历并删除item[3],那么item[4]现在是新的item[3],这并不会使任何事情变得更容易。倒序遍历时就不会出现这个问题。
当然,这是一种不使用foreach的解决方案,但重要的是要记住,“老派”的方法通常是最好的。如果由于某种原因您需要从循环中break;(且有多个项目),则for循环更有效,因为您无法从for循环中跳出。

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