concat引发的血案

之前对从concat的印象很好,因为concat方法不会更改现有数组,而是返回一个新数组。就潜意识认为concat是个很好的方法,原数组跟新数组并没有直接关系,互相不会影响。

直到上个星期五,同事离职后,交接给我一个项目和一个bug,他走之前把问题解决了,但是却没有找到为什么会出问题。我担心还有别的代码有这个问题。还是决定抽点时间看。

他具体的代码非常复杂,我还是定位了很久,现在抽象出简单的例子出来:


复现问题

下面这个例子中,发现再次使用 all 时,竟然 all 的值变了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
var users = [
{ user: 'barney', age: 36, active: true, children: {a:1} },
{ user: 'fred', age: 40, active: false },
{ user: 'travis', age: 37, active: true}
];

var users1 = [
{ user: 'seven', age: 24, active: true}
]

var allUsers = users.concat(users1)

// 利用all去做别的事情....

// 后续代码去更改 users1
var seven = _.find(users1, { user: 'seven' })

seven.age = 38

console.log(allUsers)

其实这个是不符合我们预期的,因为我们后面只是更改了 seven,并且还是 users1 的 seven, 竟然导致后面我们再使用的时候,all发生了变化。

这种问题,在代码非常大的项目里,是很难去定位的。最终改为使用 lodash 去 clone 可以解决问题。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
var users = [
{ user: 'barney', age: 36, active: true, children: {a:1} },
{ user: 'fred', age: 40, active: false },
{ user: 'travis', age: 37, active: true}
];

var users1 = [
{ user: 'seven', age: 24, active: true}
]

var allUsers = users.concat(users1)

var index = _.findIndex(users1, { user: 'seven' })
var seven = _.clone(users1[index])

seven.age = 50

// all 不受影响
console.log(allUsers)

之所以会造成这个的原因是:concat内的值如果是对象引用(而不是实际对象),concat将对象引用复制到新数组中。 原始数组和新数组都引用相同的对象。 也就是说,如果引用的对象被修改,则更改对于新数组和原始数组都是可见的。 这包括也是数组的数组参数的元素。

使用 clone 后,相当于分配了一块新的内存给新的对象,并不会影响到原来 users1 的值,因此也不会影响到 allUsers


数据类型为基础类型的 concat

如果是数据类型如字符串,数字和布尔(不是String,Number 和 Boolean 对象):concat将字符串和数字的值复制到新数组中。

1
2
3
4
5
6
7
8
9
var array1 = ['a', 'b', 'c'];
var array2 = ['d', 'e', 'f'];

var newArr = array1.concat(array2)

newArr[0] = 'j'

// array1不受影响
console.log(array1)

这一点非常像基本类型和引用类型的特性。

number、string、boolean、null和undefined型数据都是值类型。 由于值类型数据占据的空间都是固定的,所以可以把它们存储在狭窄的内存栈区。这种存储方式更方便计算机进行查找和操作,所以执行速度会非常快。

基础类型的值在从一个变量向另一个变量赋值基本类型时,会在该变量上创建一个新值,然后再把该值复制到为新变量分配的位置上。

关于引用类型和基本类型可以参考我之前写的一篇文章


总结

首先确实是我们对concat了解的太理所当然了。这也提示了我们上面这种有太多不确定性,当你确认自己不想更改原数组的值的时候,就千万不要用concat,因为后续你不小心一个赋值,可能就导致了一个bug

以下总结来自 MDN,其实别人已经写的很清楚了,是我们自己对他了解的太少。

concat方法创建一个新的数组,它由被调用的对象中的元素组成,每个参数的顺序依次是该参数的元素(如果参数是数组)或参数本身(如果参数不是数组)。它不会递归到嵌套数组参数中

concat方法不会改变this或任何作为参数提供的数组,而是返回一个浅拷贝,它包含与原始数组相结合的相同元素的副本。 原始数组的元素将复制到新数组中,如下所示:

如果是对象引用(而不是实际对象):concat将对象引用复制到新数组中。 原始数组和新数组都引用相同的对象。 也就是说,如果引用的对象被修改,则更改对于新数组和原始数组都是可见的。 这包括也是数组的数组参数的元素。

如果是数据类型如字符串,数字和布尔(不是String,Number 和 Boolean 对象):concat将字符串和数字的值复制到新数组中。