为什么展开运算符不适合复制多维数组?

6

来自MDN: 扩展语法(Spread Syntax)

注意:通常情况下,ES2015中的扩展运算符只会复制数组的一层。因此,它们不适用于复制多维数组。这也适用于Object.assign()和Object扩展语法。请参考下面的示例以更好地理解。

var a = [[1], [2], [3]];
var b = [...a];
b.shift().shift(); // 1
// Now array b is: [[2], [3]]

上述陈述的意义是什么?上面的代码示例与使用.slice()方法将数组a复制到b中完全相同。我尝试在这里向数组添加另一个维度:https://repl.it/HKOq/2,事情仍然按预期工作。

那么为什么展开语法不适用于复制多维数组呢?
感谢estus和vol7ron的答案帮助我弄清楚了问题。基本上,正如estus指出的那样,技术上只有数组内部的数组而不是多维数组。
并且正如vol7ron所解释的那样,只有数组的第一层被复制,因此对于任何进一步嵌套的元素,内存中的对象仍然保持不变。
我也错误地怀疑使用展开语法应该与slice运算符不同。

... 不是一个运算符! - Felix Kling
@FelixKling,请修改MDN文章,该文章在rest和spread语法中多次重复错误。 ...是一个标点符号,用于rest和spread语法。 - RobG
5个回答

16

程序员真的很不擅长展示能够清晰展现差别的示例。

var a = [[['a', 'b'], ['c', 'd']], 'e'];
var b = [...a];
b[0][0][0] = 'z';
b[1] = 'x';
console.log('a', a);
console.log('b', b);

这将输出:

a [[["z", "b"], ["c", "d"]], "e"]
b [[["z", "b"], ["c", "d"]], "x"]

注意到了什么奇怪的地方吗?这两个数组的 [0][0][0] 值都被更改了。这意味着在这两个数组中位于 [0][0][0] 的对象是被 引用 到同一个对象,并不是一个 副本。但是,[1] 的值是不同的,这意味着它确实是一个 副本

浅拷贝 意味着第一级别被 复制,更深层次的级别被 引用


1
太棒了,兄弟!我已经理解了这个例子,但是看到代码还是很有帮助的! - ezg
这应该被接受为最佳答案,并且也值得获得悬赏。 - Ash Archin

5

数组是对象,[...a] 创建了 浅拷贝a 数组对象。

在语言本身中,没有多维数组 - 只有数组内包含另一个数组。无论它包含的是数组、普通对象、函数还是基元类型,都没关系。对于基元类型,它们的值将被复制,否则对象引用将被复制。这就是引用块所指的内容。

Object.assign() 和 Object spread 操作符也是同样的情况

关于

上述代码示例的工作原理与使用.slice()方法将数组从a复制到b完全相同

……确实如此。这是一种更简洁的方式,可写成 a.slice()[].concat(a)。但有一个重要的例外:ES6 剩余操作符(以及 Array.from(a))适用于所有可迭代对象,而不仅仅是数组。

对于一个对象的 深拷贝,ES6 并没有提供新的方法,应该手动递归复制对象(包括数组)。为了解决所有问题,仍然有必要使用经过验证的第三方帮助函数,例如 Lodash 的 cloneDeep


所以你的意思是,在其他语言中,您可以拥有真正的多维数组,其中所有内容都在内存中是1个数组? - Gwater17
说实话,我不记得有这种语言了,将n维数组作为单独的语言实体并不太实用。如果您有其他高级编程语言的经验,您可能已经知道规则 - 数组复制通常意味着浅复制(除非证明了另外一种情况)。 - Estus Flask
2
顺便说一句,要想弄清楚ES6的工作原理,Babel和Typescript REPL是必不可少的。Babel输出更加严格符合规范,而TS输出更易于阅读;两者都非常有帮助。 - Estus Flask
1
https://developer.mozilla.org/zh-CN/docs/Web/API/structuredClone | let clone = structuredClone(arr); - Russell

1

在多维数组中,不会为内部数组元素创建新的数组:

// One-dimensional array
var a = [1,2,3];
var b = [...a];

a[0]='a';
console.log('a',a);
console.log('b',b);   
  // expected: b[0] == 1
  // got:      b[0] == 1



// Multi-dimensional array
var a = [[1], [2], [3]];
var b = [...a];

a[0][0]='a';
console.log('a',a);
console.log('b',b);   
  // expected: b[0][0] == 1
  // got:      b[0][0] == 'a'

它的工作原理类似于 slice(),因此您需要遍历数组并为每个维度创建新的数组。以下是一个快速示例:

// Multi-dimensional array
var a = [[1], [2], [3]];
var b = (function fn(ar){
 return ar.map(el=>Array.isArray(el)&&fn(el)||el) 
})(a);

a[0][0]='a';
console.log('a',a);
console.log('b',b);   
  // expected: b[0][0] == 1
  // got:      b[0][0] == 1


0

所以这个例子想要表达的是,var b = [...a]; 不会展开 a 数组中的内部数组(例如 b = [1,2,3]),而是会得到 [[1],[2],[3]]。因此,b.shift() 移除并返回 b 的第一个元素,即 [1],然后第二个 shift() 只是从返回的数组中移除了 1。简而言之,... 只能展开你的 spreaded 数组一层,例如 var b =[...a] 等同于 var b = [a[0], a[1], a[2]],而不是在这个例子中的 var b = [ a[0][0], a[1][0], a[2][0] ]


0

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