数组的push()方法和展开语法的区别

6

算法问题陈述:找到加起来等于目标总和的最小数组。

代码问题:我不理解以下两种方法在结果上的差异:

  • 使用arr.push()方法

vs.

  • 使用Spread语法

请参考下面的注释行。

Spread语法返回正确的解决方案,而.push()方法会一直推送到同一个数组。我不明白为什么它会在内存中引用同一个数组。

非常感谢!

let howSum = (target, arr, memo = {}) => {
    if (target in memo) return memo[target];
    if (target === 0) return [];
    if (target < 0) return null;

    let smallest = null;

    for (let e of arr) {
        if (howSum(target - e, arr, memo) !== null) {
            let result = howSum(target - e, arr, memo);
            // result.push(e);
            result = [...result, e];

            if (smallest === null || result.length < smallest.length) {
                smallest = result;
            }
        }
    }

    memo[target] = smallest;
    return smallest;
};

console.log(howSum(10, [1, 2, 5])); // [5, 5]

5
result = [...result, e]; 创建了一个新的数组。这并不是因为特别使用了扩展运算符,而只是因为 result = [ ] 把一个新的数组分配给了 result - Felix Kling
4
“我不明白为什么它一直在引用相同的内存数组”,这个问题的答案是这是设计上的原因。如果使用得当,通过改变对象本身而不是创建新的对象,可以获得很多便利。 - Nick
@FelixKling,谢谢。你能再详细解释一下吗?最终目的是通过添加新元素来更新数组。这两种方法都可以更新旧数组,对吗?我可以更新同一个数组,也可以创建一个带有更新值的新数组。 - Pmcorrea
1
我猜如果你不创建一个新的数组,那么每个memo[target]条目都指向同一个数组,这意味着每个target的记忆化解决方案完全相同,这显然在概念上是错误的。 - Felix Kling
2个回答

11

array.push(element)array = [...array, element]的区别

Array.push将一个元素添加到现有数组的末尾,而展开语法创建一个全新的数组。例如,以下代码会抛出错误,因为我们试图重新定义一个const

const array = ["foo", "bar"];
array = [...array, "baz"]; // Uncaught TypeError: invalid assignment to const 'array'

Array.push 可以向现有数组中添加元素,因此无需重新定义:

const array = ["foo", "bar"];
array.push("baz"); // No error; "baz" is successfully added to the end of the array.

另一个区别是速度。 array.push(element)array = [...array, element];~2,500倍

如 @FelixKling 所指出的那样,展开语法本身不会创建新数组。您也可以在函数中像这样使用展开语法:myFunction(...myArray)。这将使用数组元素作为参数。换句话说,...myArray 不会创建新数组,但 [...myArray] 会。这只是一个值得注意的小细节。


为什么循环会一直引用同一个内存中的数组

使用展开语法可以返回正确的解决方案,而使用.push()方法会不断地向同一个数组中添加元素。我不明白为什么它会一直引用同一个内存中的数组。

在JavaScript中,对象(包括JavaScript数组)是引用类型,而不是值类型。因此,使用展开语法,您创建了一个数组(result),但仍将旧数组(arr)提供给函数。当您使用Array.push时,您修改了提供给函数的数组。由于您修改了提供的数组(而不是创建一个本地数组),所以您将继续使用数组中的新值调用函数。当您使用展开语法时,您创建了一个新数组(因此result没有引用到arr数组),并且当您使用arr参数调用函数时,您仍然拥有与首次调用函数时相同的值。

@trincot 写了一个简洁的分析,解释了代码中发生了什么。

以下是您可以在JavaScript控制台中进行的实验:

const myArray = ["foo", "bar"];

function changeArray(arrayParameter) {
  const arrayVariable = arrayParameter;
  // We are still referencing the array that was supplied to our function, so
  // although it looks like we tried to duplicate the array, arrayVariable.push,
  // arrayParameter.push, and myArray.push will all modify the same array.
  arrayVariable.push("baz");
}

changeArray();
console.log(myArray); // ["foo", "bar", "baz"]

1
小修正:展开语法不创建数组,而是数组字面量创建数组。但当然,展开元素只能在数组字面量内部使用。 - Felix Kling
1
谢谢@Samuel Ebert,但是数组不是在for循环中的块作用域变量吗?因此会在下一次迭代时从内存中被擦除? - Pmcorrea
1
@SamuelEbert 谢谢,这回到了我困惑的根源。如果“result”数组在下一次迭代中被擦除,是什么导致它的引用保持“活动状态”,继续推送元素而不是擦除和重新初始化自己?如果您运行代码并记录结果数组的值,并在其下面交换已注释的行和未注释的行后再次运行它,则会看到我的意思。我假设每次迭代都会重新声明结果数组,但实际上它仍然引用原始数组。我不小心删除了您的最后一条评论,但我确实阅读了它。 - Pmcorrea
1
好的,这很尴尬。我现在非常疲倦...所以...我删除了我的另一个评论,时间太快了一秒钟。无论如何,数组是引用类型。它不能使用“push”,因为在每次迭代中调用函数时仍然引用相同的数组。因此,它不会从内存中被擦除,您只需继续修改在第一次调用中提供的数组。所以,对于混淆我再次道歉,我没有清晰地思考(由于失眠,我今晚无法入睡)。当我睡醒后,我会再次阅读您的代码。我的思维正在四处游荡。 - Sam
1
@SamuelEbert 谢谢您的答复,它帮助我理解了。希望您能休息一下,先生。 - Pmcorrea
显示剩余4条评论

3

.push()方法一直将值添加到同一个数组中,我不理解为什么它会保持对内存中相同数组的引用。

如果我们采用您的代码变体,不使用扩展语法,而是使用push,那么请注意:

  • push会改变数组本身,而不是创建一个新数组
  • 您的代码仅在return []处创建新数组

因此,请看看return []之后会发生什么:

  • result.push(e); 将更改该数组为 [e]
  • 第一次smallest为null,所以smallest成为对该单个数组的引用。
  • memo[target]处另一个对该单个数组的引用。
  • 返回此引用给调用者,并将其分配给result
  • 将一个新值推送到该单个数组中,它现在有两个元素。接下来就是关键点:
  • 由于上述memo[target]引用与result引用相同的数组,您实际上已经突变了memo[target]: memo[target].length 现在将是2。

这是不希望看到的。一旦您将数组分配给memo,它就不应该突变。因此,在您的代码中的某个地方,您应该创建一个新数组。


2
感谢您提供的逐步解释。这很有道理。我的错误在于,虽然我理解引用类型和值类型,但由于某种原因,我认为它们在循环范围内的行为不同。再次感谢! - Pmcorrea

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