深浅拷贝

2021/5/7 JavaScript

# 前言

浅拷贝和深拷贝都是对于 JavaScript 中的引用类型而言的,浅拷贝就只是复制对象的引用,如果拷贝后的对象发生变化,原对象也会发生变化。只有深拷贝才是真正地对对象的拷贝。

说到深浅拷贝,必须先提到的是 JavaScript 的数据类型。

需要知道的就是一点:JavaScript 的数据类型分为基本数据类型和引用数据类型。

对于基本数据类型的拷贝,并没有深浅拷贝的区别,我们所说的深浅拷贝都是对于引用数据类型而言的。

# 浅拷贝

浅拷贝的意思就是只复制引用,而未复制真正的值。

const originArray = [1,2,3,4,5];
const originObj = {a:'a',b:'b',c:[1,2,3],d:{dd:'dd'}};

const cloneArray = originArray;
const cloneObj = originObj;

console.log(cloneArray); // [1,2,3,4,5]
console.log(originObj); // {a:'a',b:'b',c:Array[3],d:{dd:'dd'}}

cloneArray.push(6);
cloneObj.a = {aa:'aa'};

console.log(cloneArray); // [1,2,3,4,5,6]
console.log(originArray); // [1,2,3,4,5,6]

console.log(cloneObj); // {a:{aa:'aa'},b:'b',c:Array[3],d:{dd:'dd'}}
console.log(originArray); // {a:{aa:'aa'},b:'b',c:Array[3],d:{dd:'dd'}}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

上面的代码是最简单的利用 = 赋值操作符实现了一个浅拷贝,可以很清楚的看到,随着 cloneArraycloneObj 改变,originArrayoriginObj 也随着发生了变化。

# 深拷贝

深拷贝就是对目标的完全拷贝,不像浅拷贝那样只是复制了一层引用,就连值也都复制了。

目前实现深拷贝的方法不多,主要是两种:

  1. 利用 JSON 对象中的 parsestringify
  2. 利用递归来实现每一层都重新创建对象并赋值
  3. 使用 lodash.cloneDeep 这类工具实现深拷贝,有工具不用,哎,放着玩儿?

# JSON.stringify/parse 的方法

先看看这两个方法吧:

The JSON.stringify() method converts a JavaScript value to a JSON string.

JSON.stringify 是将一个 JavaScript 值转成一个 JSON 字符串。

The JSON.parse() method parses a JSON string, constructing the JavaScript value or object described by the string.

JSON.parse 是将一个 JSON 字符串转成一个 JavaScript 值或对象。

很好理解吧,就是 JavaScript 值和 JSON 字符串的相互转换。

const originArray = [1,2,3,4,5];
const cloneArray = JSON.parse(JSON.stringify(originArray));
console.log(cloneArray === originArray); // false

const originObj = {a:'a',b:'b',c:[1,2,3],d:{dd:'dd'}};
const cloneObj = JSON.parse(JSON.stringify(originObj));
console.log(cloneObj === originObj); // false

cloneObj.a = 'aa';
cloneObj.c = [1,1,1];
cloneObj.d.dd = 'doubled';

console.log(cloneObj); // {a:'aa',b:'b',c:[1,1,1],d:{dd:'doubled'}};
console.log(originObj); // {a:'a',b:'b',c:[1,2,3],d:{dd:'dd'}};
1
2
3
4
5
6
7
8
9
10
11
12
13
14

确实是深拷贝,也很方便。但是,这个方法只能适用于一些简单的情况。比如下面这样的一个对象就不适用:

const originObj = {
  name:'axuebin',
  sayHello:function(){
    console.log('Hello World');
  }
}
console.log(originObj); // {name: "axuebin", sayHello: ƒ}
const cloneObj = JSON.parse(JSON.stringify(originObj));
console.log(cloneObj); // {name: "axuebin"}
1
2
3
4
5
6
7
8
9

发现在 cloneObj 中,有属性丢失了。。。那是为什么呢?

MDN 上找到了原因:

If undefined, a function, or a symbol is encountered during conversion it is either omitted (when it is found in an object) or censored to null (when it is found in an array). JSON.stringify can also just return undefined when passing in "pure" values like JSON.stringify(function(){}) or JSON.stringify(undefined).

undefinedfunctionsymbol 会在转换过程中被忽略。

明白了吧,就是说如果对象中含有一个函数时(很常见),就不能用这个方法进行深拷贝。

# 递归的方法

递归的思想就很简单了,就是对每一层的数据都实现一次 创建对象->对象赋值 的操作,简单粗暴上代码:

function deepClone(source){
  const targetObj = source.constructor === Array ? [] : {}; // 判断复制的目标是数组还是对象
  for(let keys in source){ // 遍历目标
    if(source.hasOwnProperty(keys)){
      if(source[keys] && typeof source[keys] === 'object'){ // 如果值是对象,就递归一下
        targetObj[keys] = source[keys].constructor === Array ? [] : {};
        targetObj[keys] = deepClone(source[keys]);
      }else{ // 如果不是,就直接赋值
        targetObj[keys] = source[keys];
      }
    } 
  }
  return targetObj;
}

// 用 ES6 方法
function deepClone(source) {
  const clone = source.constructor === Array ? [] : {};
  Object.keys(source).forEach(key => {
    clone[key] = typeof source[key] === 'object' ? deepClone(source[key]) : source[key]
  });
  return clone;
}

// 另外一个
function deepClone(origin) {
  if(origin === null) return null;
  if(typeof origin !== 'object') return origin;
  if(origin.constructor === Date) return new Date(origin); 
  // 保持继承链
  let _target = origin.constructor();
  // 循环 origin
  for(let key in origin) {
    // 不遍历其原型链上的属性
    if (origin.hasOwnProperty(key)) {
      // 如果 origin[key] 是一个引用类型的值,则进入递归逻辑
      if (typeof origin[key] === 'object' && origin[key] !== null) {
        // 进入递归,此时原始值就是 origin[key],被赋值的对象是 _target[key]
        // 注意,上述第一次声明的 _target 将会贯穿整个递归,后续所有的赋值,都将会被 return 到 _target
        _target[key] = deepClone(origin[key]);
      } else {
        // 如果不是对象或数组,则进入此逻辑,直接赋值给 _target[key]
        _target[key] = origin[key];
      }
    }
  }
  // for...in 循环结束后,return 当前上下文的 _target 值
  return _target;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49

# JavaScript 中的拷贝方法

我们知道在 JavaScript 中,数组有两个方法 concatslice 是可以实现对原数组的拷贝的,这两个方法都不会修改原数组,而是返回一个修改后的新数组。

同时,ES6 中 引入了 Object.assgn 方法和 ... 展开运算符也能实现对对象的拷贝。

那它们是浅拷贝还是深拷贝呢?

# concat

The concat() method is used to merge two or more arrays. This method does not change the existing arrays, but instead returns a new array.

该方法可以连接两个或者更多的数组,但是它不会修改已存在的数组,而是返回一个新数组。

结论:concat 只是对数组的第一层进行深拷贝。

# slice

The slice() method returns a shallow copy of a portion of an array into a new array object selected from begin to end (end not included). The original array will not be modified.

解释中都直接写道是 a shallow copy 了 ~

结论:slice 只是对数组的第一层进行深拷贝。

# Object.assign()

The Object.assign() method is used to copy the values of all enumerable own properties from one or more source objects to a target object. It will return the target object.

复制复制复制。那到底是浅拷贝还是深拷贝呢?

结论:Object.assign() 拷贝的是属性值。假如源对象的属性值是一个指向对象的引用,它也只拷贝那个引用值。

# ... 展开运算符

结论:... 实现的是对象第一层的深拷贝。后面的只是拷贝的引用值。

# 总结

  1. 赋值运算符 = 实现的是浅拷贝,只拷贝对象的引用值;
  2. JavaScript 中数组和对象自带的拷贝方法都是“首层浅拷贝”;
  3. JSON.stringify 实现的是深拷贝,但是对目标对象有要求;
  4. 若想真正意义上的深拷贝,请递归。
Last Updated: 2023/04/22, 23:39:26
彩虹
周杰伦