深浅拷贝

83 阅读4分钟

前言:既然说深浅拷贝,就要先理解js到底有那数据类型

数据类型

  • 基本类型

    七种基本数据类型UndefinedNullBooleanNumberStringsymbolbigint

    变量是直接按值存放的,存放在栈内存中的简单数据段,可以直接访问。

  • 引用类型

    对象类型、函数,对象类型又分为:Object对象、Array数组、RegExp正则、Date时间对象、Math数学对象

    存放在堆内存中的对象,变量保存的是一个指针,这个指针指向另一个位置。当需要访问引用类型(如对象,数组等)的值时,首先从栈中获得该对象的地址指针,然后再从堆内存中取得所需的数据

面试题:如何判断数据类型

typeof

  • 能够准确检查除了 null 之外的基础数据类型(number, string, symbol, bigInt, undefined, boolean, null)
  • 缺点:无法区分引用数据 数组 对象 null 都是'object'

instanceof

  • instanceof 是用来判断 A 是否为 B 的实例
  • instanceof 只能用来判断两个对象是否属于实例关系, 而不能判断一个对象实例具体属于哪种类型。

constructor

  • null 和 undefined 是无效的对象,因此是不会有 constructor 存在的,这两种类型的数据需要通过其他方式来判断。
  • 函数的 constructor 是不稳定的,这个主要体现在自定义对象上,当开发者重写 prototype 后,原有的 constructor 引用会丢失,constructor 会默认为 Object

Object.prototype.toString.call 无敌

回到正题 浅拷贝 vs 深拷贝

浅拷贝: 只复制引用,而未复制真正的值。

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'}}

利用 = 赋值操作符实现了一个浅拷贝,可以很清楚的看到,随着 cloneArray 和 cloneObj 改变,originArray 和 originObj 也随着发生了变化。

深拷贝: 就连值也都复制了不会存在相互影响

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

  • 利用 JSON 对象中的 parse 和 stringify
  • 利用递归来实现每一层都重新创建对象并赋值

JSON.stringify/parse的方法

先看看这两个方法吧:

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

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

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'}};

确实是深拷贝,也很方便。但是,这个方法只能适用于一些简单的情况。

undefinedfunctionsymbol 会在转换过程中被忽略

递归的方法

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

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

JavaScript中的拷贝方法

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

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

concatslice

  • concat该方法可以连接两个或者更多的数组
  • lice 对数组进行切割

//concat
const originArray = [1,2,3,4,5];
const cloneArray = originArray.concat();
console.log(cloneArray === originArray); // false
cloneArray.push(6); // [1,2,3,4,5,6]
console.log(originArray); [1,2,3,4,5];

//slice
const cloneArray2 = originArray.slice()
console.log(cloneArray2  === originArray)//false
cloneArray2.push(6); // [1,2,3,4,5,6]
console.log(originArray); [1,2,3,4,5];

看上去是深拷贝的。但我们试一下多层

const originArray = [1,[1,2,3],{a:1}];
const cloneArray = originArray.concat();
console.log(cloneArray === originArray); // false
cloneArray[1].push(4);
cloneArray[2].a = 2; 
console.log(originArray); // [1,[1,2,3,4],{a:2}]

const cloneArray2 = originArray.slice();
console.log(cloneArray2 === originArray); // false
cloneArray2[1].push(4);
cloneArray2[2].a = 2; 
console.log(originArray2); // [1,[1,2,3,4],{a:2}]

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

Object.assign()

const originObj ={a:1,b:{bb:2}}
const cloneObj = Object.assign({},originObj)
cloneObj.b.bb = 3
console.log(originObj) //{a:1,b:{bb:3}}

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

... 展开运算符

const originArray = [1,2,3,4,5,[6,7,8]];
const originObj = {a:1,b:{bb:1}};

const cloneArray = [...originArray];
cloneArray[0] = 0;
cloneArray[5].push(9);
console.log(originArray); // [1,2,3,4,5,[6,7,8,9]]

const cloneObj = {...originObj};
cloneObj.a = 2;
cloneObj.b.bb = 2;
console.log(originObj); // {a:1,b:{bb:2}}

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