lxq.link
postscategoriestoolsabout

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
2020-07-20