为什么对数组进行js map操作会修改原始数组?

63

我对map()的行为感到非常困惑。

我有这样一个对象数组:

const products = [{
    ...,
    'productType' = 'premium',
    ...
}, ...]

我将这个数组传递给一个函数,该函数应返回相同的数组,但其中所有产品都是免费的:

And I'm passing this array to a function that should return the same array but with all product made free:
[{
    ...,
    'productType' = 'free',
    ...
}, ...]

该函数的作用是:

const freeProduct = function(products){
    return products.map(x => x.productType = "free")
}

这将返回以下数组:

["free", "free", ...]

所以我重新编写了我的函数,使其变成:

const freeProduct = function(products){
    return products.map(x => {x.productType = "free"; return x})
}

虽然这个功能可以按预期返回数组,但这就是我失去理智的时刻,在这两种情况下,我的原始产品数组都被修改了。

有关map()的文档表示它不应该修改原始数组(https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/map)。

我甚至尝试创建数组的克隆,将我的函数变成这样:

const freeProduct = function(products){
    p = products.splice()
    return p.map(x => {x.productType = "free"; return x})
}

但我仍然得到相同的结果(这开始让我发疯)。

如果有人能为我解释我做错了什么,我将非常感激!

谢谢。

5个回答

116

你没有修改原始数组,你正在修改数组中的对象。如果你想避免改变数组中的对象,可以使用Object.assign创建一个新对象,该对象具有原始对象的属性以及任何所需的更改:

const freeProduct = function(products) {
  return products.map(x => {
    return Object.assign({}, x, {
      productType: "free"
    });
  });
};

2018年修订:

大多数浏览器中,您现在可以使用对象展开语法来代替Object.assign来完成此操作:

const freeProduct = function(products) {
  return products.map(x => {
    return {
      ...x,
      productType: "free"
    };
  });
};

1
@HarshaKanchina 这是在ES6中添加的箭头函数语法。这里是关于它的一些信息,可以在MDN上找到。 - SimpleJ
1
Javascript 假装是一种函数式语言,但与 class 关键字一样,这很令人困惑,不直观,并且浪费了很多时间。 “你没有修改原始数组。你只是修改了数组中的对象。” - 尽管这个答案是正确的,但它在逻辑上是不合理的,因为在现实世界中,你不能改变某物的一部分并使其保持不变。 - Cobolt
8
这并不是不合逻辑,因为该数组不包含对象,而是包含对对象的引用。这些引用保持不变,因此原始数组也保持不变。被引用的对象是被修改的。想想书中的单词,语言随时间演变,单词的意义可能会改变,但书本身是一样的。 - foxinatardis
现有的数组进行筛选后,使用 .map() 函数处理其中的元素。 我通常使用这段代码作为内联解决方案,相对于大部分情况下的克隆,它更快捷。由于 filter 函数返回一个新的数组,因此不会修改原有数组。 - Asqan
1
@Asqan 在您发布的代码中,.filter(e => e) 只是生成一个新数组,其中 falsey (null, 0, undefined,等) 值被删除。Map 也会生成一个新数组,因此,除非您想过滤掉 falsey 值,否则在这里使用 filter 是没有用处的。 - SimpleJ

17
进一步解释SimpleJ的答案 - 如果您将两个数组 ===,您会发现它们不相等(在内存中没有相同的地址),从而确认映射数组实际上是一个新数组。问题在于,您正在返回一个新数组,其中包含对原始数组中相同对象的引用(它不返回新的对象文字,而是返回对同一对象的引用)。因此,您需要创建旧对象的副本的新对象 - 即,使用SimpleJ给出的Object.assign示例。

5
很遗憾,无论是展开运算符还是对象赋值运算符都不会进行深度复制。你需要使用类似lodash的函数来获得一个真正的复制而不仅仅是引用复制。
const util = require('util');
const print = (...val) => {
    console.log(util.inspect(val, false, null, false /* enable colors */));
};
const _ = require('lodash');

const obj1 =     {foo:{bar:[{foo:3}]}};
const obj2 =     {foo:{bar:[{foo:3}]}};

const array = [obj1, obj2];

const objAssignCopy = x => { return Object.assign({}, x, {})};
const spreadCopy = x => { return {...x}};
const _Copy = x => _.cloneDeep(x);

const map1 = array.map(objAssignCopy);
const map2 = array.map(spreadCopy);
const map3 = array.map(_Copy);

print('map1', map1);
print('map2', map2);
print('map3', map3);
obj2.foo.bar[0].foo = "foobar";
print('map1 after manipulation of obj2', map1); // value changed 
print('map2 after manipulation of obj2', map2); // value changed
print('map3 after manipulation of obj2', map3); // value hasn't changed!

2

数组迭代器Array.map()创建具有相同元素数量的新数组或不更改原始数组。如果数组中有对象,则可能存在引用问题,因为它会复制相同的引用,因此,当您对对象的属性进行任何更改时,它将更改保持相同引用的元素的原始值。

解决方案是复制对象,好吧,array.Splice()[...array](Spread运算符)在这种情况下无法帮助,您可以使用JavaScript实用程序库,例如Loadash或仅使用下面提到的代码:

const newList = JSON.parse(JSON.stringify(orinalArr))

1

数组解构赋值可以用来克隆对象。

const freeProduct = function(products){
    p = products.splice()
    return p.map(({...x}) => {x.productType = "free"; return x})
}

这种方法不会修改原始对象。


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