JS的数组复制,实现浅拷贝和深拷贝
数组复制是常见的功能之一,比如下面这个数组arr,我们现在想把它赋值给一个新变量 arr_copy
let arr = [
'a',
{ key: 1 },
function(){ console.log('exec') }
]
第一种方法,直接赋值:
let arr_copy = arr;
js 里面将数组/对象赋值给一个新变量,都是拷贝原始数组/对象的内存地址,新旧变量指向同一个指针。这样赋值后,原数组/对象和新变量的更新会同步,其实就是我们常说的浅拷贝。
arr[0] = 'new_a';
arr[1] = 1
arr[2] = false
console.log(arr)
// [ 'new_a', 1, false ]
console.log(arr_copy)
// [ 'new_a', 1, false ]
这里看一下打印结果,确实数组的每一项都同步被更改。但很多时候,我们其实不想要这样的同步更改,新数组和旧数组之间互不影响。实现这样的功能,就是深拷贝。
在说深拷贝之前,先说一些误区。
JS数组有一些自带的方法不会操作原数组,而是返回一个新数组。比如 slice / concat / es6新增的拓展运算符(...)。
在想实现数组深拷贝的时候,可能首先会想到这些方法,如果用这些方法,就陷入误区了。这些方法不会输出预想的结果。
我们先看第二种复制数组的方法 concat()
let arr_copy = arr.concat();
更改原数组里面的字符串和对象
arr[0] = 'new_a';
arr[1].key = 10;
console.log(arr)
// [ 'new_a', { key: 10 }, [Function] ]
console.log(arr_copy)
// [ 'a', { key: 10 }, [Function] ]
可以看到数组第一项字符串的更改没有互相产生影响,第二项对象的更改却同步了。
为什么会这样呢,这里直接引用 MDN 文档说明:
concat方法不会改变this或任何作为参数提供的数组,而是返回一个浅拷贝,它包含与原始数组相结合的相同元素的副本。 原始数组的元素将复制到新数组中,如下所示:
- 对象引用(而不是实际对象):concat将对象引用复制到新数组中。 原始数组和新数组都引用相同的对象。 也就是说,如果引用的对象被修改,则更改对于新数组和原始数组都是可见的。 这包括也是数组的数组参数的元素。
- 数据类型如字符串,数字和布尔(不是String,Number 和 Boolean 对象):concat将字符串和数字的值复制到新数组中。
所以 concat 方法是浅拷贝。
常见的数组slice方法,拓展运算符和对象的Object.assign 方法也是浅拷贝。
// slice
let arr_copy = arr.slice(0);
arr[0] = 'new_a';
arr[1].key = 10;
console.log(arr)
// [ 'new_a', { key: 10 }, [Function] ]
console.log(arr_copy)
// [ 'a', { key: 10 }, [Function] ]
// 拓展运算符
let arr_copy = [...arr];
arr[0] = 'new_a';
arr[1].key = 10;
console.log(arr)
// [ 'new_a', { key: 10 }, [Function] ]
console.log(arr_copy)
// [ 'a', { key: 10 }, [Function] ]
// 对象Object.assign
let obj = {
key1 : 'str',
key2 : {
obj_key : 1
}
}
let obj_copy = Object.assign(obj)
obj.key1 = 'str_new'
obj.key2.obj_key = 10
console.log(obj)
// { key1: 'str_new', key2: { obj_key: 10 } }
console.log(obj_copy)
// { key1: 'str_new', key2: { obj_key: 10 } }
想实现深拷贝,不能用上面这些方法。
把数组和对象转成字符串再解析回来能实现深拷贝:
let arr = [
'a',
{ key: 1 },
]
let arr_copy = JSON.parse(JSON.stringify(arr));
arr[0] = 'new_a';
arr[1].key = 10;
console.log(arr)
// [ 'new_a', { key: 10 } ]
console.log(arr_copy)
// [ 'a', { key: 1 } ]
这里看到打印结果符合预期,两个数组之间没有同步更改。
当数组里面有函数的数据类型呢
let arr = [
'a',
{ key: 1 },
function(){ console.log('exec') }
]
let arr_copy = JSON.parse(JSON.stringify(arr));
arr[0] = 'new_a';
arr[1].key = 10;
arr[2] = function() {console.log('exec new')}
console.log(arr)
// [ 'new_a', { key: 10 }, [Function] ]
arr[2]()
// exec new
console.log(arr_copy)
// [ 'a', { key: 1 } ]
新数组没有同步更改,但是函数类型出问题了,转换完变成了null。所以 JSON stringify parse 是有缺陷的。
更通用的方法是写一个方法递归更新:
const deepClone = function (param) {
let t = Object.prototype.toString
let n
if (t.call(param) === '[object Array]') {
n = []
} else if (t.call(param) === '[object Object]') {
n = {}
} else {
return param
}
for (let i in param) {
if (param.hasOwnProperty(i)) {
if (
t.call(param[i]) === '[object Array]' ||
t.call(param[i]) === '[object Object]'
) {
n[i] = deepClone(param[i])
} else {
n[i] = param[i]
}
}
}
return n
}
let arr = ['a', { key: 1 }, function(){ console.log('exec') }]
let arr_copy = deepClone(arr)
arr[0] = 'c'
arr[1].key = 10
arr[2] = function() {console.log('exec new')}
console.log(arr)
// [ 'c', { key: 10 }, [Function] ]
arr[2]()
// exec new
console.log(arr_copy)
// [ 'a', { key: 1 }, [Function] ]
arr_copy[2]()
// exec